@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.
@@ -1878,7 +1878,6 @@ var AbracadabraWS = class extends EventEmitter {
1878
1878
  if (this.connectionAttempt) this.rejectConnectionAttempt();
1879
1879
  this.status = WebSocketStatus.Disconnected;
1880
1880
  this.emit("status", { status: WebSocketStatus.Disconnected });
1881
- console.log("[DEBUG] onClose event:", typeof event, JSON.stringify(event), "code:", event?.code);
1882
1881
  const isRateLimited = event?.code === 4429 || event === 4429;
1883
1882
  this.emit("disconnect", { event });
1884
1883
  if (isRateLimited) this.emit("rateLimited");
@@ -2936,7 +2935,7 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
2936
2935
  const childProvider = new AbracadabraProvider({
2937
2936
  name: childId,
2938
2937
  document: childDoc,
2939
- url: this.abracadabraConfig.url ?? this.configuration.websocketProvider?.url,
2938
+ websocketProvider: this.configuration.websocketProvider,
2940
2939
  token: this.configuration.token,
2941
2940
  subdocLoading: this.subdocLoading,
2942
2941
  disableOfflineStore: this.abracadabraConfig.disableOfflineStore,
@@ -2946,6 +2945,7 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
2946
2945
  docKeyManager: this.abracadabraConfig.docKeyManager,
2947
2946
  keystore: this.abracadabraConfig.keystore
2948
2947
  });
2948
+ childProvider.attach();
2949
2949
  this.childProviders.set(childId, childProvider);
2950
2950
  this.emit("subdocLoaded", {
2951
2951
  childId,
@@ -6914,7 +6914,7 @@ function fromBase64url(b64) {
6914
6914
  const DB_NAME = "abracadabra:identity";
6915
6915
  const STORE_NAME = "identity";
6916
6916
  const RECORD_KEY = "current";
6917
- const HKDF_INFO$1 = new TextEncoder().encode("abracadabra-identity-v1");
6917
+ const HKDF_INFO$2 = new TextEncoder().encode("abracadabra-identity-v1");
6918
6918
  function openDb$4() {
6919
6919
  return new Promise((resolve, reject) => {
6920
6920
  const req = indexedDB.open(DB_NAME, 1);
@@ -6947,7 +6947,7 @@ async function dbDelete(db) {
6947
6947
  });
6948
6948
  }
6949
6949
  async function deriveAesKey(prfOutput, salt) {
6950
- const keyBytes = hkdf(sha256, new Uint8Array(prfOutput), salt, HKDF_INFO$1, 32);
6950
+ const keyBytes = hkdf(sha256, new Uint8Array(prfOutput), salt, HKDF_INFO$2, 32);
6951
6951
  return crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
6952
6952
  }
6953
6953
  var CryptoIdentityKeystore = class {
@@ -7664,7 +7664,7 @@ var FileBlobStore = class extends EventEmitter {
7664
7664
  * Manages AES-256-GCM document keys for CSE and E2E encrypted documents.
7665
7665
  * Keys are wrapped per-user using X25519 ECDH + HKDF-SHA256 + AES-256-GCM.
7666
7666
  */
7667
- const HKDF_INFO = new TextEncoder().encode("abracadabra-dockey-v1");
7667
+ const HKDF_INFO$1 = new TextEncoder().encode("abracadabra-dockey-v1");
7668
7668
  function fromBase64$1(b64) {
7669
7669
  return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
7670
7670
  }
@@ -7708,7 +7708,7 @@ var DocKeyManager = class {
7708
7708
  async wrapKeyForRecipient(docKey, recipientX25519PubKey, docId) {
7709
7709
  const ephemeralPriv = crypto.getRandomValues(new Uint8Array(32));
7710
7710
  const ephemeralPub = x25519.getPublicKey(ephemeralPriv);
7711
- const keyBytes = hkdf(sha256, x25519.getSharedSecret(ephemeralPriv, recipientX25519PubKey), new TextEncoder().encode(docId), HKDF_INFO, 32);
7711
+ const keyBytes = hkdf(sha256, x25519.getSharedSecret(ephemeralPriv, recipientX25519PubKey), new TextEncoder().encode(docId), HKDF_INFO$1, 32);
7712
7712
  const wrapKey = await crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt"]);
7713
7713
  const rawDocKey = await crypto.subtle.exportKey("raw", docKey);
7714
7714
  const nonce = crypto.getRandomValues(new Uint8Array(12));
@@ -7726,7 +7726,7 @@ var DocKeyManager = class {
7726
7726
  const ephemeralPub = wrapped.slice(0, 32);
7727
7727
  const nonce = wrapped.slice(32, 44);
7728
7728
  const ciphertext = wrapped.slice(44);
7729
- const keyBytes = hkdf(sha256, x25519.getSharedSecret(recipientX25519PrivKey, ephemeralPub), new TextEncoder().encode(docId), HKDF_INFO, 32);
7729
+ const keyBytes = hkdf(sha256, x25519.getSharedSecret(recipientX25519PrivKey, ephemeralPub), new TextEncoder().encode(docId), HKDF_INFO$1, 32);
7730
7730
  const wrapKey = await crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["decrypt"]);
7731
7731
  const rawDocKey = await crypto.subtle.decrypt({
7732
7732
  name: "AES-GCM",
@@ -8696,15 +8696,23 @@ const SHA256_BYTES = 32;
8696
8696
 
8697
8697
  //#endregion
8698
8698
  //#region packages/provider/src/webrtc/DataChannelRouter.ts
8699
+ /** Name of the data channel used for E2EE key exchange. */
8700
+ const KEY_EXCHANGE_CHANNEL = "key-exchange";
8699
8701
  var DataChannelRouter = class extends EventEmitter {
8700
8702
  constructor(connection) {
8701
8703
  super();
8702
8704
  this.connection = connection;
8703
8705
  this.channels = /* @__PURE__ */ new Map();
8706
+ this.encryptor = null;
8707
+ this.plaintextChannels = new Set([KEY_EXCHANGE_CHANNEL]);
8704
8708
  this.connection.ondatachannel = (event) => {
8705
8709
  this.registerChannel(event.channel);
8706
8710
  };
8707
8711
  }
8712
+ /** Attach an E2EE encryptor. All channels (except key-exchange) will be encrypted. */
8713
+ setEncryptor(encryptor) {
8714
+ this.encryptor = encryptor;
8715
+ }
8708
8716
  /** Create a named data channel (initiator side). */
8709
8717
  createChannel(name, options) {
8710
8718
  const channel = this.connection.createDataChannel(name, options);
@@ -8720,12 +8728,35 @@ var DataChannelRouter = class extends EventEmitter {
8720
8728
  });
8721
8729
  if (opts.enableFileTransfer) this.createChannel(CHANNEL_NAMES.FILE_TRANSFER, { ordered: true });
8722
8730
  }
8731
+ /**
8732
+ * Create namespaced channels for a child/subdocument.
8733
+ * Channel names are prefixed with `{childId}:` to avoid collisions.
8734
+ */
8735
+ createSubdocChannels(childId, opts) {
8736
+ if (opts.enableDocSync) this.createChannel(`${childId}:${CHANNEL_NAMES.YJS_SYNC}`, { ordered: true });
8737
+ if (opts.enableAwareness) this.createChannel(`${childId}:${CHANNEL_NAMES.AWARENESS}`, {
8738
+ ordered: false,
8739
+ maxRetransmits: 0
8740
+ });
8741
+ }
8723
8742
  getChannel(name) {
8724
8743
  return this.channels.get(name) ?? null;
8725
8744
  }
8726
8745
  isOpen(name) {
8727
8746
  return this.channels.get(name)?.readyState === "open";
8728
8747
  }
8748
+ /**
8749
+ * Send data on a named channel, encrypting if E2EE is active.
8750
+ * Falls back to plaintext if no encryptor is set or for exempt channels.
8751
+ */
8752
+ async send(name, data) {
8753
+ const channel = this.channels.get(name);
8754
+ if (!channel || channel.readyState !== "open") return;
8755
+ if (this.encryptor?.isEstablished && !this.plaintextChannels.has(name)) {
8756
+ const encrypted = await this.encryptor.encrypt(data);
8757
+ channel.send(encrypted);
8758
+ } else channel.send(data);
8759
+ }
8729
8760
  registerChannel(channel) {
8730
8761
  channel.binaryType = "arraybuffer";
8731
8762
  this.channels.set(channel.label, channel);
@@ -8739,10 +8770,22 @@ var DataChannelRouter = class extends EventEmitter {
8739
8770
  this.emit("channelClose", { name: channel.label });
8740
8771
  this.channels.delete(channel.label);
8741
8772
  };
8742
- channel.onmessage = (event) => {
8773
+ channel.onmessage = async (event) => {
8774
+ const name = channel.label;
8775
+ let data = event.data;
8776
+ if (this.encryptor?.isEstablished && !this.plaintextChannels.has(name) && (data instanceof ArrayBuffer || data instanceof Uint8Array)) try {
8777
+ const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
8778
+ data = await this.encryptor.decrypt(buf);
8779
+ } catch (err) {
8780
+ this.emit("channelError", {
8781
+ name,
8782
+ error: err
8783
+ });
8784
+ return;
8785
+ }
8743
8786
  this.emit("channelMessage", {
8744
- name: channel.label,
8745
- data: event.data
8787
+ name,
8788
+ data
8746
8789
  });
8747
8790
  };
8748
8791
  channel.onerror = (event) => {
@@ -8870,7 +8913,13 @@ var PeerConnection = class extends EventEmitter {
8870
8913
  * prevent echo loops with the server-based provider.
8871
8914
  */
8872
8915
  var YjsDataChannel = class {
8873
- constructor(document, awareness, router) {
8916
+ /**
8917
+ * @param document - The Y.Doc to sync
8918
+ * @param awareness - Optional Awareness instance
8919
+ * @param router - DataChannelRouter for the peer connection
8920
+ * @param channelPrefix - Optional prefix for subdocument channels (e.g. `"{childId}:"`)
8921
+ */
8922
+ constructor(document, awareness, router, channelPrefix) {
8874
8923
  this.document = document;
8875
8924
  this.awareness = awareness;
8876
8925
  this.router = router;
@@ -8878,52 +8927,49 @@ var YjsDataChannel = class {
8878
8927
  this.awarenessUpdateHandler = null;
8879
8928
  this.channelOpenHandler = null;
8880
8929
  this.channelMessageHandler = null;
8930
+ const prefix = channelPrefix ?? "";
8931
+ this.syncChannelName = `${prefix}${CHANNEL_NAMES.YJS_SYNC}`;
8932
+ this.awarenessChannelName = `${prefix}${CHANNEL_NAMES.AWARENESS}`;
8881
8933
  }
8882
8934
  /** Start listening for Y.js updates and data channel messages. */
8883
8935
  attach() {
8884
8936
  this.docUpdateHandler = (update, origin) => {
8885
8937
  if (origin === this) return;
8886
- const channel = this.router.getChannel(CHANNEL_NAMES.YJS_SYNC);
8887
- if (!channel || channel.readyState !== "open") return;
8938
+ if (!this.router.isOpen(this.syncChannelName)) return;
8888
8939
  const encoder = createEncoder();
8889
8940
  writeVarUint(encoder, YJS_MSG.UPDATE);
8890
8941
  writeVarUint8Array(encoder, update);
8891
- channel.send(toUint8Array(encoder));
8942
+ this.router.send(this.syncChannelName, toUint8Array(encoder));
8892
8943
  };
8893
8944
  this.document.on("update", this.docUpdateHandler);
8894
8945
  if (this.awareness) {
8895
8946
  this.awarenessUpdateHandler = ({ added, updated, removed }, _origin) => {
8896
- const channel = this.router.getChannel(CHANNEL_NAMES.AWARENESS);
8897
- if (!channel || channel.readyState !== "open") return;
8947
+ if (!this.router.isOpen(this.awarenessChannelName)) return;
8898
8948
  const changedClients = added.concat(updated).concat(removed);
8899
8949
  const update = encodeAwarenessUpdate(this.awareness, changedClients);
8900
- channel.send(update);
8950
+ this.router.send(this.awarenessChannelName, update);
8901
8951
  };
8902
8952
  this.awareness.on("update", this.awarenessUpdateHandler);
8903
8953
  }
8904
8954
  this.channelMessageHandler = ({ name, data }) => {
8905
- if (name === CHANNEL_NAMES.YJS_SYNC) this.handleSyncMessage(data);
8906
- else if (name === CHANNEL_NAMES.AWARENESS) this.handleAwarenessMessage(data);
8955
+ if (name === this.syncChannelName) this.handleSyncMessage(data);
8956
+ else if (name === this.awarenessChannelName) this.handleAwarenessMessage(data);
8907
8957
  };
8908
8958
  this.router.on("channelMessage", this.channelMessageHandler);
8909
8959
  this.channelOpenHandler = ({ name }) => {
8910
- if (name === CHANNEL_NAMES.YJS_SYNC) this.sendSyncStep1();
8911
- else if (name === CHANNEL_NAMES.AWARENESS && this.awareness) {
8912
- const channel = this.router.getChannel(CHANNEL_NAMES.AWARENESS);
8913
- if (channel?.readyState === "open") {
8960
+ if (name === this.syncChannelName) this.sendSyncStep1();
8961
+ else if (name === this.awarenessChannelName && this.awareness) {
8962
+ if (this.router.isOpen(this.awarenessChannelName)) {
8914
8963
  const update = encodeAwarenessUpdate(this.awareness, Array.from(this.awareness.getStates().keys()));
8915
- channel.send(update);
8964
+ this.router.send(this.awarenessChannelName, update);
8916
8965
  }
8917
8966
  }
8918
8967
  };
8919
8968
  this.router.on("channelOpen", this.channelOpenHandler);
8920
- if (this.router.isOpen(CHANNEL_NAMES.YJS_SYNC)) this.sendSyncStep1();
8921
- if (this.awareness && this.router.isOpen(CHANNEL_NAMES.AWARENESS)) {
8922
- const channel = this.router.getChannel(CHANNEL_NAMES.AWARENESS);
8923
- if (channel?.readyState === "open") {
8924
- const update = encodeAwarenessUpdate(this.awareness, Array.from(this.awareness.getStates().keys()));
8925
- channel.send(update);
8926
- }
8969
+ if (this.router.isOpen(this.syncChannelName)) this.sendSyncStep1();
8970
+ if (this.awareness && this.router.isOpen(this.awarenessChannelName)) {
8971
+ const update = encodeAwarenessUpdate(this.awareness, Array.from(this.awareness.getStates().keys()));
8972
+ this.router.send(this.awarenessChannelName, update);
8927
8973
  }
8928
8974
  }
8929
8975
  /** Stop listening and clean up handlers. */
@@ -8950,12 +8996,11 @@ var YjsDataChannel = class {
8950
8996
  this.detach();
8951
8997
  }
8952
8998
  sendSyncStep1() {
8953
- const channel = this.router.getChannel(CHANNEL_NAMES.YJS_SYNC);
8954
- if (!channel || channel.readyState !== "open") return;
8999
+ if (!this.router.isOpen(this.syncChannelName)) return;
8955
9000
  const encoder = createEncoder();
8956
9001
  writeVarUint(encoder, YJS_MSG.SYNC);
8957
9002
  writeSyncStep1(encoder, this.document);
8958
- channel.send(toUint8Array(encoder));
9003
+ this.router.send(this.syncChannelName, toUint8Array(encoder));
8959
9004
  }
8960
9005
  handleSyncMessage(data) {
8961
9006
  const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
@@ -8968,8 +9013,7 @@ var YjsDataChannel = class {
8968
9013
  const responseEncoder = createEncoder();
8969
9014
  writeVarUint(responseEncoder, YJS_MSG.SYNC);
8970
9015
  writeUint8Array(responseEncoder, toUint8Array(encoder));
8971
- const channel = this.router.getChannel(CHANNEL_NAMES.YJS_SYNC);
8972
- if (channel?.readyState === "open") channel.send(toUint8Array(responseEncoder));
9016
+ if (this.router.isOpen(this.syncChannelName)) this.router.send(this.syncChannelName, toUint8Array(responseEncoder));
8973
9017
  }
8974
9018
  if (syncMessageType === messageYjsSyncStep2) this.isSynced = true;
8975
9019
  } else if (msgType === YJS_MSG.UPDATE) {
@@ -9219,6 +9263,136 @@ function constantTimeEqual(a, b) {
9219
9263
  return result === 0;
9220
9264
  }
9221
9265
 
9266
+ //#endregion
9267
+ //#region packages/provider/src/webrtc/E2EEChannel.ts
9268
+ /**
9269
+ * E2EEChannel
9270
+ *
9271
+ * Per-peer end-to-end encryption for WebRTC data channels using
9272
+ * X25519 ECDH key agreement + HKDF-SHA256 + AES-256-GCM.
9273
+ *
9274
+ * Leverages the same cryptographic primitives as `DocKeyManager` and
9275
+ * `CryptoIdentityKeystore` but applied to data channel messages rather
9276
+ * than stored document keys.
9277
+ *
9278
+ * Key agreement flow:
9279
+ * 1. Both peers exchange Ed25519 public keys via the `key-exchange` data channel
9280
+ * 2. Convert Ed25519 → X25519 (Montgomery form)
9281
+ * 3. Run X25519 ECDH to derive a shared secret
9282
+ * 4. HKDF-SHA256(sharedSecret, salt, info) → 32-byte AES-256-GCM session key
9283
+ * 5. All subsequent data channel messages are encrypted with this key
9284
+ *
9285
+ * Message wire format: [12-byte nonce || AES-256-GCM ciphertext]
9286
+ *
9287
+ * Dependencies: @noble/curves (already in the project for CryptoIdentityKeystore)
9288
+ */
9289
+ const HKDF_INFO = new TextEncoder().encode("abracadabra-webrtc-e2ee-v1");
9290
+ const NONCE_BYTES = 12;
9291
+ var E2EEChannel = class extends EventEmitter {
9292
+ constructor(identity, docId) {
9293
+ super();
9294
+ this.identity = identity;
9295
+ this.docId = docId;
9296
+ this.sessionKey = null;
9297
+ this.remotePublicKey = null;
9298
+ this._isEstablished = false;
9299
+ }
9300
+ get isEstablished() {
9301
+ return this._isEstablished;
9302
+ }
9303
+ /**
9304
+ * Process a key-exchange message from the remote peer.
9305
+ * Called when the `key-exchange` data channel receives a message.
9306
+ *
9307
+ * The message is the remote peer's raw 32-byte Ed25519 public key.
9308
+ * After receiving it, ECDH is computed and the session key derived.
9309
+ */
9310
+ async handleKeyExchange(remoteEdPubKey) {
9311
+ if (remoteEdPubKey.length !== 32) {
9312
+ this.emit("error", /* @__PURE__ */ new Error("Invalid remote public key length"));
9313
+ return;
9314
+ }
9315
+ this.remotePublicKey = remoteEdPubKey;
9316
+ const remoteX25519Pub = ed25519.utils.toMontgomery(remoteEdPubKey);
9317
+ const sharedSecret = x25519.getSharedSecret(this.identity.x25519PrivateKey, remoteX25519Pub);
9318
+ const localPub = this.identity.publicKey;
9319
+ const remotePub = remoteEdPubKey;
9320
+ const [first, second] = this.sortKeys(localPub, remotePub);
9321
+ const saltParts = [
9322
+ new TextEncoder().encode(this.docId),
9323
+ first,
9324
+ second
9325
+ ];
9326
+ const salt = new Uint8Array(saltParts.reduce((acc, p) => acc + p.length, 0));
9327
+ let offset = 0;
9328
+ for (const part of saltParts) {
9329
+ salt.set(part, offset);
9330
+ offset += part.length;
9331
+ }
9332
+ const keyBytes = hkdf(sha256, sharedSecret, salt, HKDF_INFO, 32);
9333
+ this.sessionKey = await crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
9334
+ this._isEstablished = true;
9335
+ this.emit("established", { remotePublicKey: remoteEdPubKey });
9336
+ }
9337
+ /**
9338
+ * Returns the local Ed25519 public key to send to the remote peer
9339
+ * via the `key-exchange` data channel.
9340
+ */
9341
+ getKeyExchangeMessage() {
9342
+ return this.identity.publicKey;
9343
+ }
9344
+ /**
9345
+ * Encrypt a message for sending over a data channel.
9346
+ * Returns `[12-byte nonce || AES-256-GCM ciphertext]`.
9347
+ *
9348
+ * @throws if the session key has not been established yet.
9349
+ */
9350
+ async encrypt(plaintext) {
9351
+ if (!this.sessionKey) throw new Error("E2EE session key not established");
9352
+ const nonce = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
9353
+ const ciphertext = new Uint8Array(await crypto.subtle.encrypt({
9354
+ name: "AES-GCM",
9355
+ iv: nonce
9356
+ }, this.sessionKey, plaintext));
9357
+ const result = new Uint8Array(NONCE_BYTES + ciphertext.length);
9358
+ result.set(nonce, 0);
9359
+ result.set(ciphertext, NONCE_BYTES);
9360
+ return result;
9361
+ }
9362
+ /**
9363
+ * Decrypt a message received from a data channel.
9364
+ * Expects `[12-byte nonce || AES-256-GCM ciphertext]`.
9365
+ *
9366
+ * @throws if the session key has not been established or decryption fails.
9367
+ */
9368
+ async decrypt(data) {
9369
+ if (!this.sessionKey) throw new Error("E2EE session key not established");
9370
+ if (data.length < NONCE_BYTES + 16) throw new Error("E2EE ciphertext too short");
9371
+ const nonce = data.slice(0, NONCE_BYTES);
9372
+ const ciphertext = data.slice(NONCE_BYTES);
9373
+ const plaintext = await crypto.subtle.decrypt({
9374
+ name: "AES-GCM",
9375
+ iv: nonce
9376
+ }, this.sessionKey, ciphertext);
9377
+ return new Uint8Array(plaintext);
9378
+ }
9379
+ /** Destroy the session key and wipe sensitive material. */
9380
+ destroy() {
9381
+ this.sessionKey = null;
9382
+ this.remotePublicKey = null;
9383
+ this._isEstablished = false;
9384
+ this.removeAllListeners();
9385
+ }
9386
+ /** Sort two keys lexicographically so both peers produce the same order. */
9387
+ sortKeys(a, b) {
9388
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
9389
+ if (a[i] < b[i]) return [a, b];
9390
+ if (a[i] > b[i]) return [b, a];
9391
+ }
9392
+ return a.length <= b.length ? [a, b] : [b, a];
9393
+ }
9394
+ };
9395
+
9222
9396
  //#endregion
9223
9397
  //#region packages/provider/src/webrtc/AbracadabraWebRTC.ts
9224
9398
  const HAS_RTC = typeof globalThis.RTCPeerConnection !== "undefined";
@@ -9239,6 +9413,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9239
9413
  this.peerConnections = /* @__PURE__ */ new Map();
9240
9414
  this.yjsChannels = /* @__PURE__ */ new Map();
9241
9415
  this.fileChannels = /* @__PURE__ */ new Map();
9416
+ this.e2eeChannels = /* @__PURE__ */ new Map();
9242
9417
  this.peers = /* @__PURE__ */ new Map();
9243
9418
  this.localPeerId = null;
9244
9419
  this.isConnected = false;
@@ -9247,6 +9422,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9247
9422
  this.config = {
9248
9423
  docId: configuration.docId,
9249
9424
  url: configuration.url,
9425
+ signalingUrl: configuration.signalingUrl ?? null,
9250
9426
  token: configuration.token,
9251
9427
  document: doc,
9252
9428
  awareness,
@@ -9257,6 +9433,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9257
9433
  enableAwarenessSync: configuration.enableAwarenessSync ?? !!awareness,
9258
9434
  enableFileTransfer: configuration.enableFileTransfer ?? false,
9259
9435
  fileChunkSize: configuration.fileChunkSize ?? 16384,
9436
+ e2ee: configuration.e2ee ?? null,
9260
9437
  WebSocketPolyfill: configuration.WebSocketPolyfill
9261
9438
  };
9262
9439
  if (configuration.autoConnect !== false && HAS_RTC) this.connect();
@@ -9437,6 +9614,11 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9437
9614
  }
9438
9615
  removePeer(peerId) {
9439
9616
  this.peers.delete(peerId);
9617
+ const e2ee = this.e2eeChannels.get(peerId);
9618
+ if (e2ee) {
9619
+ e2ee.destroy();
9620
+ this.e2eeChannels.delete(peerId);
9621
+ }
9440
9622
  const yjs = this.yjsChannels.get(peerId);
9441
9623
  if (yjs) {
9442
9624
  yjs.destroy();
@@ -9492,12 +9674,39 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9492
9674
  return pc;
9493
9675
  }
9494
9676
  attachDataHandlers(peerId, pc) {
9677
+ if (this.config.e2ee) {
9678
+ const e2ee = new E2EEChannel(this.config.e2ee, this.config.docId);
9679
+ this.e2eeChannels.set(peerId, e2ee);
9680
+ pc.router.setEncryptor(e2ee);
9681
+ pc.router.on("channelMessage", async ({ name, data }) => {
9682
+ if (name === KEY_EXCHANGE_CHANNEL) {
9683
+ const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
9684
+ await e2ee.handleKeyExchange(buf);
9685
+ }
9686
+ });
9687
+ pc.router.on("channelOpen", ({ name, channel }) => {
9688
+ if (name === KEY_EXCHANGE_CHANNEL) channel.send(e2ee.getKeyExchangeMessage());
9689
+ });
9690
+ e2ee.on("established", () => {
9691
+ this.emit("e2eeEstablished", { peerId });
9692
+ this.startDataSync(peerId, pc);
9693
+ });
9694
+ e2ee.on("error", (err) => {
9695
+ this.emit("e2eeFailed", {
9696
+ peerId,
9697
+ error: err
9698
+ });
9699
+ });
9700
+ } else this.startDataSync(peerId, pc);
9701
+ }
9702
+ startDataSync(peerId, pc) {
9495
9703
  if (this.config.document && this.config.enableDocSync) {
9704
+ if (this.yjsChannels.has(peerId)) return;
9496
9705
  const yjs = new YjsDataChannel(this.config.document, this.config.enableAwarenessSync ? this.config.awareness : null, pc.router);
9497
9706
  yjs.attach();
9498
9707
  this.yjsChannels.set(peerId, yjs);
9499
9708
  }
9500
- if (this.config.enableFileTransfer) {
9709
+ if (this.config.enableFileTransfer && !this.fileChannels.has(peerId)) {
9501
9710
  const fc = new FileTransferChannel(pc.router, this.config.fileChunkSize);
9502
9711
  fc.on("receiveStart", (meta) => {
9503
9712
  this.emit("fileReceiveStart", {
@@ -9534,6 +9743,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9534
9743
  }
9535
9744
  async initiateConnection(peerId) {
9536
9745
  const pc = this.createPeerConnection(peerId);
9746
+ if (this.config.e2ee) pc.router.createChannel(KEY_EXCHANGE_CHANNEL, { ordered: true });
9537
9747
  pc.router.createDefaultChannels({
9538
9748
  enableDocSync: this.config.enableDocSync,
9539
9749
  enableAwareness: this.config.enableAwarenessSync,
@@ -9565,6 +9775,12 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9565
9775
  }
9566
9776
  }
9567
9777
  buildSignalingUrl() {
9778
+ if (this.config.signalingUrl) {
9779
+ let sigBase = this.config.signalingUrl;
9780
+ while (sigBase.endsWith("/")) sigBase = sigBase.slice(0, -1);
9781
+ sigBase = sigBase.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
9782
+ return `${sigBase}/ws/${encodeURIComponent(this.config.docId)}/signaling`;
9783
+ }
9568
9784
  let base = this.config.url;
9569
9785
  while (base.endsWith("/")) base = base.slice(0, -1);
9570
9786
  base = base.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
@@ -9573,5 +9789,309 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9573
9789
  };
9574
9790
 
9575
9791
  //#endregion
9576
- export { AbracadabraBaseProvider, AbracadabraClient, AbracadabraProvider, AbracadabraWS, AbracadabraWebRTC, AuthMessageType, AwarenessError, BackgroundSyncManager, BackgroundSyncPersistence, CHANNEL_NAMES, ConnectionTimeout, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, DocKeyManager, DocumentCache, E2EAbracadabraProvider, E2EOfflineStore, EncryptedYMap, EncryptedYText, FileBlobStore, FileTransferChannel, FileTransferHandle, Forbidden, HocuspocusProvider, HocuspocusProviderWebsocket, MessageTooBig, MessageType, OfflineStore, PeerConnection, ResetConnection, SearchIndex, SignalingSocket, SubdocMessage, Unauthorized, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, decryptField, encryptField, makeEncryptedYMap, makeEncryptedYText, readAuthMessage, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest };
9792
+ //#region packages/provider/src/webrtc/ManualSignaling.ts
9793
+ /**
9794
+ * ManualSignaling
9795
+ *
9796
+ * Serverless signaling adapter for WebRTC peer-to-peer connections.
9797
+ * Instead of a WebSocket server relaying SDP/ICE, peers exchange
9798
+ * offer and answer "blobs" out-of-band (QR code, copy-paste, NFC, etc.).
9799
+ *
9800
+ * Designed as a drop-in replacement for `SignalingSocket` — emits the
9801
+ * same events (`welcome`, `joined`, `offer`, `answer`, `ice`) so
9802
+ * `AbracadabraWebRTC` can use it transparently.
9803
+ *
9804
+ * Flow:
9805
+ * Device A (initiator):
9806
+ * 1. `createOffer()` → gathers ICE candidates → returns offer blob
9807
+ * 2. Share blob via QR/paste
9808
+ * 3. Receive answer blob → `acceptAnswer(blob)`
9809
+ *
9810
+ * Device B (responder):
9811
+ * 1. Receive offer blob → `acceptOffer(blob)` → returns answer blob
9812
+ * 2. Share answer blob via QR/paste
9813
+ */
9814
+ var ManualSignaling = class extends EventEmitter {
9815
+ constructor(iceServers) {
9816
+ super();
9817
+ this.isConnected = false;
9818
+ this.pc = null;
9819
+ this.localPeerId = crypto.randomUUID();
9820
+ this.iceServers = iceServers ?? [{ urls: "stun:stun.l.google.com:19302" }];
9821
+ }
9822
+ /**
9823
+ * Initiator: create an offer blob with gathered ICE candidates.
9824
+ * Returns a blob to share with the remote peer.
9825
+ */
9826
+ async createOfferBlob() {
9827
+ this.pc = new RTCPeerConnection({ iceServers: this.iceServers });
9828
+ const candidates = [];
9829
+ const gatheringComplete = new Promise((resolve) => {
9830
+ this.pc.onicecandidate = (event) => {
9831
+ if (event.candidate) candidates.push(JSON.stringify(event.candidate.toJSON()));
9832
+ else resolve();
9833
+ };
9834
+ });
9835
+ const offer = await this.pc.createOffer();
9836
+ await this.pc.setLocalDescription(offer);
9837
+ await gatheringComplete;
9838
+ this.isConnected = true;
9839
+ this.emit("welcome", {
9840
+ peerId: this.localPeerId,
9841
+ peers: []
9842
+ });
9843
+ return {
9844
+ sdp: this.pc.localDescription.sdp,
9845
+ candidates,
9846
+ peerId: this.localPeerId
9847
+ };
9848
+ }
9849
+ /**
9850
+ * Responder: accept an offer blob and create an answer blob.
9851
+ * The answer blob should be shared back to the initiator.
9852
+ */
9853
+ async acceptOffer(offerBlob) {
9854
+ this.pc = new RTCPeerConnection({ iceServers: this.iceServers });
9855
+ const candidates = [];
9856
+ const gatheringComplete = new Promise((resolve) => {
9857
+ this.pc.onicecandidate = (event) => {
9858
+ if (event.candidate) candidates.push(JSON.stringify(event.candidate.toJSON()));
9859
+ else resolve();
9860
+ };
9861
+ });
9862
+ await this.pc.setRemoteDescription(new RTCSessionDescription({
9863
+ type: "offer",
9864
+ sdp: offerBlob.sdp
9865
+ }));
9866
+ for (const c of offerBlob.candidates) await this.pc.addIceCandidate(new RTCIceCandidate(JSON.parse(c)));
9867
+ const answer = await this.pc.createAnswer();
9868
+ await this.pc.setLocalDescription(answer);
9869
+ await gatheringComplete;
9870
+ this.isConnected = true;
9871
+ this.emit("welcome", {
9872
+ peerId: this.localPeerId,
9873
+ peers: []
9874
+ });
9875
+ this.emit("joined", {
9876
+ peer_id: offerBlob.peerId,
9877
+ user_id: offerBlob.peerId,
9878
+ muted: false,
9879
+ video: false,
9880
+ screen: false,
9881
+ name: null,
9882
+ color: null
9883
+ });
9884
+ this.emit("offer", {
9885
+ from: offerBlob.peerId,
9886
+ sdp: offerBlob.sdp
9887
+ });
9888
+ for (const c of offerBlob.candidates) this.emit("ice", {
9889
+ from: offerBlob.peerId,
9890
+ candidate: c
9891
+ });
9892
+ return {
9893
+ sdp: this.pc.localDescription.sdp,
9894
+ candidates,
9895
+ peerId: this.localPeerId
9896
+ };
9897
+ }
9898
+ /**
9899
+ * Initiator: accept the answer blob from the responder.
9900
+ * Completes the connection.
9901
+ */
9902
+ async acceptAnswer(answerBlob) {
9903
+ if (!this.pc) throw new Error("Call createOfferBlob() first");
9904
+ this.emit("joined", {
9905
+ peer_id: answerBlob.peerId,
9906
+ user_id: answerBlob.peerId,
9907
+ muted: false,
9908
+ video: false,
9909
+ screen: false,
9910
+ name: null,
9911
+ color: null
9912
+ });
9913
+ this.emit("answer", {
9914
+ from: answerBlob.peerId,
9915
+ sdp: answerBlob.sdp
9916
+ });
9917
+ for (const c of answerBlob.candidates) this.emit("ice", {
9918
+ from: answerBlob.peerId,
9919
+ candidate: c
9920
+ });
9921
+ }
9922
+ sendOffer(_to, _sdp) {}
9923
+ sendAnswer(_to, _sdp) {}
9924
+ sendIce(_to, _candidate) {}
9925
+ sendMute(_muted) {}
9926
+ sendMediaState(_video, _screen) {}
9927
+ sendProfile(_name, _color) {}
9928
+ sendLeave() {}
9929
+ async connect() {}
9930
+ disconnect() {
9931
+ this.isConnected = false;
9932
+ if (this.pc) {
9933
+ this.pc.close();
9934
+ this.pc = null;
9935
+ }
9936
+ this.emit("disconnected");
9937
+ }
9938
+ destroy() {
9939
+ this.disconnect();
9940
+ this.removeAllListeners();
9941
+ }
9942
+ };
9943
+
9944
+ //#endregion
9945
+ //#region packages/provider/src/sync/BroadcastChannelSync.ts
9946
+ /**
9947
+ * Cross-tab Y.js document and awareness sync via the BroadcastChannel API.
9948
+ *
9949
+ * Opens a BroadcastChannel per document, relays Y.js updates and awareness
9950
+ * state between tabs on the same origin. No server, no WebRTC — just same-device
9951
+ * multi-tab sync. Same-origin means same trust boundary, so no encryption needed.
9952
+ *
9953
+ * Uses the same y-protocols/sync encoding as `YjsDataChannel` for consistency.
9954
+ */
9955
+ const CHANNEL_PREFIX = "abra:sync:";
9956
+ /** Message type discriminators (first byte). */
9957
+ const MSG = {
9958
+ SYNC: 0,
9959
+ UPDATE: 1,
9960
+ AWARENESS: 2,
9961
+ QUERY_PEERS: 3
9962
+ };
9963
+ var BroadcastChannelSync = class BroadcastChannelSync extends EventEmitter {
9964
+ constructor(document, awareness, channelName) {
9965
+ super();
9966
+ this.document = document;
9967
+ this.awareness = awareness;
9968
+ this.channelName = channelName;
9969
+ this.channel = null;
9970
+ this.docUpdateHandler = null;
9971
+ this.awarenessUpdateHandler = null;
9972
+ this.destroyed = false;
9973
+ }
9974
+ /** Convenience factory using standard channel naming. */
9975
+ static forDoc(document, docId, awareness) {
9976
+ return new BroadcastChannelSync(document, awareness ?? null, `${CHANNEL_PREFIX}${docId}`);
9977
+ }
9978
+ /** Start syncing. Opens the BroadcastChannel and initiates a sync handshake. */
9979
+ connect() {
9980
+ if (this.destroyed || this.channel) return;
9981
+ if (typeof globalThis.BroadcastChannel === "undefined") return;
9982
+ this.channel = new BroadcastChannel(this.channelName);
9983
+ this.channel.onmessage = (event) => this.handleMessage(event.data);
9984
+ this.docUpdateHandler = (update, origin) => {
9985
+ if (origin === this) return;
9986
+ this.broadcastUpdate(update);
9987
+ };
9988
+ this.document.on("update", this.docUpdateHandler);
9989
+ if (this.awareness) {
9990
+ this.awarenessUpdateHandler = ({ added, updated, removed }, _origin) => {
9991
+ const changedClients = added.concat(updated).concat(removed);
9992
+ const update = encodeAwarenessUpdate(this.awareness, changedClients);
9993
+ this.broadcastAwareness(update);
9994
+ };
9995
+ this.awareness.on("update", this.awarenessUpdateHandler);
9996
+ }
9997
+ this.broadcastQueryPeers();
9998
+ this.sendSyncStep1();
9999
+ this.emit("connected");
10000
+ }
10001
+ /** Stop syncing and close the BroadcastChannel. */
10002
+ disconnect() {
10003
+ if (this.docUpdateHandler) {
10004
+ this.document.off("update", this.docUpdateHandler);
10005
+ this.docUpdateHandler = null;
10006
+ }
10007
+ if (this.awarenessUpdateHandler && this.awareness) {
10008
+ this.awareness.off("update", this.awarenessUpdateHandler);
10009
+ this.awarenessUpdateHandler = null;
10010
+ }
10011
+ if (this.channel) {
10012
+ this.channel.close();
10013
+ this.channel = null;
10014
+ }
10015
+ this.emit("disconnected");
10016
+ }
10017
+ /** Disconnect and prevent reconnection. */
10018
+ destroy() {
10019
+ this.disconnect();
10020
+ this.destroyed = true;
10021
+ this.removeAllListeners();
10022
+ }
10023
+ broadcastUpdate(update) {
10024
+ if (!this.channel) return;
10025
+ const encoder = createEncoder();
10026
+ writeVarUint(encoder, MSG.UPDATE);
10027
+ writeVarUint8Array(encoder, update);
10028
+ this.channel.postMessage(toUint8Array(encoder));
10029
+ }
10030
+ broadcastAwareness(update) {
10031
+ if (!this.channel) return;
10032
+ const encoder = createEncoder();
10033
+ writeVarUint(encoder, MSG.AWARENESS);
10034
+ writeVarUint8Array(encoder, update);
10035
+ this.channel.postMessage(toUint8Array(encoder));
10036
+ }
10037
+ sendSyncStep1() {
10038
+ if (!this.channel) return;
10039
+ const encoder = createEncoder();
10040
+ writeVarUint(encoder, MSG.SYNC);
10041
+ writeSyncStep1(encoder, this.document);
10042
+ this.channel.postMessage(toUint8Array(encoder));
10043
+ }
10044
+ broadcastQueryPeers() {
10045
+ if (!this.channel) return;
10046
+ const encoder = createEncoder();
10047
+ writeVarUint(encoder, MSG.QUERY_PEERS);
10048
+ this.channel.postMessage(toUint8Array(encoder));
10049
+ }
10050
+ handleMessage(data) {
10051
+ if (!(data instanceof ArrayBuffer || data instanceof Uint8Array)) return;
10052
+ const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
10053
+ const decoder = createDecoder(buf);
10054
+ switch (readVarUint(decoder)) {
10055
+ case MSG.SYNC:
10056
+ this.handleSyncMessage(decoder);
10057
+ break;
10058
+ case MSG.UPDATE:
10059
+ this.handleUpdateMessage(decoder);
10060
+ break;
10061
+ case MSG.AWARENESS:
10062
+ this.handleAwarenessMessage(decoder);
10063
+ break;
10064
+ case MSG.QUERY_PEERS:
10065
+ this.sendSyncStep1();
10066
+ if (this.awareness) {
10067
+ const update = encodeAwarenessUpdate(this.awareness, Array.from(this.awareness.getStates().keys()));
10068
+ this.broadcastAwareness(update);
10069
+ }
10070
+ break;
10071
+ }
10072
+ }
10073
+ handleSyncMessage(decoder) {
10074
+ const encoder = createEncoder();
10075
+ const syncMessageType = readSyncMessage(decoder, encoder, this.document, this);
10076
+ if (length(encoder) > 0) {
10077
+ const responseEncoder = createEncoder();
10078
+ writeVarUint(responseEncoder, MSG.SYNC);
10079
+ writeUint8Array(responseEncoder, toUint8Array(encoder));
10080
+ this.channel?.postMessage(toUint8Array(responseEncoder));
10081
+ }
10082
+ if (syncMessageType === messageYjsSyncStep2) this.emit("synced");
10083
+ }
10084
+ handleUpdateMessage(decoder) {
10085
+ const update = readVarUint8Array(decoder);
10086
+ Y.applyUpdate(this.document, update, this);
10087
+ }
10088
+ handleAwarenessMessage(decoder) {
10089
+ if (!this.awareness) return;
10090
+ const update = readVarUint8Array(decoder);
10091
+ applyAwarenessUpdate(this.awareness, update, this);
10092
+ }
10093
+ };
10094
+
10095
+ //#endregion
10096
+ export { AbracadabraBaseProvider, AbracadabraClient, AbracadabraProvider, AbracadabraWS, AbracadabraWebRTC, AuthMessageType, AwarenessError, BackgroundSyncManager, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ConnectionTimeout, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, DocKeyManager, DocumentCache, E2EAbracadabraProvider, E2EEChannel, E2EOfflineStore, EncryptedYMap, EncryptedYText, FileBlobStore, FileTransferChannel, FileTransferHandle, Forbidden, HocuspocusProvider, HocuspocusProviderWebsocket, KEY_EXCHANGE_CHANNEL, ManualSignaling, MessageTooBig, MessageType, OfflineStore, PeerConnection, ResetConnection, SearchIndex, SignalingSocket, SubdocMessage, Unauthorized, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, decryptField, encryptField, makeEncryptedYMap, makeEncryptedYText, readAuthMessage, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest };
9577
10097
  //# sourceMappingURL=abracadabra-provider.esm.js.map