@abraca/dabra 1.0.4 → 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.
- package/dist/abracadabra-provider.cjs +561 -36
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +558 -37
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +163 -2
- package/package.json +1 -1
- package/src/AbracadabraProvider.ts +11 -8
- package/src/index.ts +1 -0
- package/src/sync/BroadcastChannelSync.ts +235 -0
- package/src/webrtc/AbracadabraWebRTC.ts +68 -1
- package/src/webrtc/DataChannelRouter.ts +73 -5
- package/src/webrtc/E2EEChannel.ts +195 -0
- package/src/webrtc/ManualSignaling.ts +197 -0
- package/src/webrtc/YjsDataChannel.ts +37 -30
- package/src/webrtc/index.ts +5 -1
- package/src/webrtc/types.ts +18 -0
|
@@ -2965,7 +2965,7 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2965
2965
|
const childProvider = new AbracadabraProvider({
|
|
2966
2966
|
name: childId,
|
|
2967
2967
|
document: childDoc,
|
|
2968
|
-
|
|
2968
|
+
websocketProvider: this.configuration.websocketProvider,
|
|
2969
2969
|
token: this.configuration.token,
|
|
2970
2970
|
subdocLoading: this.subdocLoading,
|
|
2971
2971
|
disableOfflineStore: this.abracadabraConfig.disableOfflineStore,
|
|
@@ -2975,6 +2975,7 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2975
2975
|
docKeyManager: this.abracadabraConfig.docKeyManager,
|
|
2976
2976
|
keystore: this.abracadabraConfig.keystore
|
|
2977
2977
|
});
|
|
2978
|
+
childProvider.attach();
|
|
2978
2979
|
this.childProviders.set(childId, childProvider);
|
|
2979
2980
|
this.emit("subdocLoaded", {
|
|
2980
2981
|
childId,
|
|
@@ -6943,7 +6944,7 @@ function fromBase64url(b64) {
|
|
|
6943
6944
|
const DB_NAME = "abracadabra:identity";
|
|
6944
6945
|
const STORE_NAME = "identity";
|
|
6945
6946
|
const RECORD_KEY = "current";
|
|
6946
|
-
const HKDF_INFO$
|
|
6947
|
+
const HKDF_INFO$2 = new TextEncoder().encode("abracadabra-identity-v1");
|
|
6947
6948
|
function openDb$4() {
|
|
6948
6949
|
return new Promise((resolve, reject) => {
|
|
6949
6950
|
const req = indexedDB.open(DB_NAME, 1);
|
|
@@ -6976,7 +6977,7 @@ async function dbDelete(db) {
|
|
|
6976
6977
|
});
|
|
6977
6978
|
}
|
|
6978
6979
|
async function deriveAesKey(prfOutput, salt) {
|
|
6979
|
-
const keyBytes = hkdf(sha256, new Uint8Array(prfOutput), salt, HKDF_INFO$
|
|
6980
|
+
const keyBytes = hkdf(sha256, new Uint8Array(prfOutput), salt, HKDF_INFO$2, 32);
|
|
6980
6981
|
return crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
6981
6982
|
}
|
|
6982
6983
|
var CryptoIdentityKeystore = class {
|
|
@@ -7693,7 +7694,7 @@ var FileBlobStore = class extends EventEmitter {
|
|
|
7693
7694
|
* Manages AES-256-GCM document keys for CSE and E2E encrypted documents.
|
|
7694
7695
|
* Keys are wrapped per-user using X25519 ECDH + HKDF-SHA256 + AES-256-GCM.
|
|
7695
7696
|
*/
|
|
7696
|
-
const HKDF_INFO = new TextEncoder().encode("abracadabra-dockey-v1");
|
|
7697
|
+
const HKDF_INFO$1 = new TextEncoder().encode("abracadabra-dockey-v1");
|
|
7697
7698
|
function fromBase64$1(b64) {
|
|
7698
7699
|
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
7699
7700
|
}
|
|
@@ -7737,7 +7738,7 @@ var DocKeyManager = class {
|
|
|
7737
7738
|
async wrapKeyForRecipient(docKey, recipientX25519PubKey, docId) {
|
|
7738
7739
|
const ephemeralPriv = crypto.getRandomValues(new Uint8Array(32));
|
|
7739
7740
|
const ephemeralPub = x25519.getPublicKey(ephemeralPriv);
|
|
7740
|
-
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);
|
|
7741
7742
|
const wrapKey = await crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt"]);
|
|
7742
7743
|
const rawDocKey = await crypto.subtle.exportKey("raw", docKey);
|
|
7743
7744
|
const nonce = crypto.getRandomValues(new Uint8Array(12));
|
|
@@ -7755,7 +7756,7 @@ var DocKeyManager = class {
|
|
|
7755
7756
|
const ephemeralPub = wrapped.slice(0, 32);
|
|
7756
7757
|
const nonce = wrapped.slice(32, 44);
|
|
7757
7758
|
const ciphertext = wrapped.slice(44);
|
|
7758
|
-
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);
|
|
7759
7760
|
const wrapKey = await crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["decrypt"]);
|
|
7760
7761
|
const rawDocKey = await crypto.subtle.decrypt({
|
|
7761
7762
|
name: "AES-GCM",
|
|
@@ -8747,15 +8748,23 @@ const SHA256_BYTES = 32;
|
|
|
8747
8748
|
|
|
8748
8749
|
//#endregion
|
|
8749
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";
|
|
8750
8753
|
var DataChannelRouter = class extends EventEmitter {
|
|
8751
8754
|
constructor(connection) {
|
|
8752
8755
|
super();
|
|
8753
8756
|
this.connection = connection;
|
|
8754
8757
|
this.channels = /* @__PURE__ */ new Map();
|
|
8758
|
+
this.encryptor = null;
|
|
8759
|
+
this.plaintextChannels = new Set([KEY_EXCHANGE_CHANNEL]);
|
|
8755
8760
|
this.connection.ondatachannel = (event) => {
|
|
8756
8761
|
this.registerChannel(event.channel);
|
|
8757
8762
|
};
|
|
8758
8763
|
}
|
|
8764
|
+
/** Attach an E2EE encryptor. All channels (except key-exchange) will be encrypted. */
|
|
8765
|
+
setEncryptor(encryptor) {
|
|
8766
|
+
this.encryptor = encryptor;
|
|
8767
|
+
}
|
|
8759
8768
|
/** Create a named data channel (initiator side). */
|
|
8760
8769
|
createChannel(name, options) {
|
|
8761
8770
|
const channel = this.connection.createDataChannel(name, options);
|
|
@@ -8771,12 +8780,35 @@ var DataChannelRouter = class extends EventEmitter {
|
|
|
8771
8780
|
});
|
|
8772
8781
|
if (opts.enableFileTransfer) this.createChannel(CHANNEL_NAMES.FILE_TRANSFER, { ordered: true });
|
|
8773
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
|
+
}
|
|
8774
8794
|
getChannel(name) {
|
|
8775
8795
|
return this.channels.get(name) ?? null;
|
|
8776
8796
|
}
|
|
8777
8797
|
isOpen(name) {
|
|
8778
8798
|
return this.channels.get(name)?.readyState === "open";
|
|
8779
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
|
+
}
|
|
8780
8812
|
registerChannel(channel) {
|
|
8781
8813
|
channel.binaryType = "arraybuffer";
|
|
8782
8814
|
this.channels.set(channel.label, channel);
|
|
@@ -8790,10 +8822,22 @@ var DataChannelRouter = class extends EventEmitter {
|
|
|
8790
8822
|
this.emit("channelClose", { name: channel.label });
|
|
8791
8823
|
this.channels.delete(channel.label);
|
|
8792
8824
|
};
|
|
8793
|
-
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
|
+
}
|
|
8794
8838
|
this.emit("channelMessage", {
|
|
8795
|
-
name
|
|
8796
|
-
data
|
|
8839
|
+
name,
|
|
8840
|
+
data
|
|
8797
8841
|
});
|
|
8798
8842
|
};
|
|
8799
8843
|
channel.onerror = (event) => {
|
|
@@ -8921,7 +8965,13 @@ var PeerConnection = class extends EventEmitter {
|
|
|
8921
8965
|
* prevent echo loops with the server-based provider.
|
|
8922
8966
|
*/
|
|
8923
8967
|
var YjsDataChannel = class {
|
|
8924
|
-
|
|
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) {
|
|
8925
8975
|
this.document = document;
|
|
8926
8976
|
this.awareness = awareness;
|
|
8927
8977
|
this.router = router;
|
|
@@ -8929,52 +8979,49 @@ var YjsDataChannel = class {
|
|
|
8929
8979
|
this.awarenessUpdateHandler = null;
|
|
8930
8980
|
this.channelOpenHandler = null;
|
|
8931
8981
|
this.channelMessageHandler = null;
|
|
8982
|
+
const prefix = channelPrefix ?? "";
|
|
8983
|
+
this.syncChannelName = `${prefix}${CHANNEL_NAMES.YJS_SYNC}`;
|
|
8984
|
+
this.awarenessChannelName = `${prefix}${CHANNEL_NAMES.AWARENESS}`;
|
|
8932
8985
|
}
|
|
8933
8986
|
/** Start listening for Y.js updates and data channel messages. */
|
|
8934
8987
|
attach() {
|
|
8935
8988
|
this.docUpdateHandler = (update, origin) => {
|
|
8936
8989
|
if (origin === this) return;
|
|
8937
|
-
|
|
8938
|
-
if (!channel || channel.readyState !== "open") return;
|
|
8990
|
+
if (!this.router.isOpen(this.syncChannelName)) return;
|
|
8939
8991
|
const encoder = createEncoder();
|
|
8940
8992
|
writeVarUint(encoder, YJS_MSG.UPDATE);
|
|
8941
8993
|
writeVarUint8Array(encoder, update);
|
|
8942
|
-
|
|
8994
|
+
this.router.send(this.syncChannelName, toUint8Array(encoder));
|
|
8943
8995
|
};
|
|
8944
8996
|
this.document.on("update", this.docUpdateHandler);
|
|
8945
8997
|
if (this.awareness) {
|
|
8946
8998
|
this.awarenessUpdateHandler = ({ added, updated, removed }, _origin) => {
|
|
8947
|
-
|
|
8948
|
-
if (!channel || channel.readyState !== "open") return;
|
|
8999
|
+
if (!this.router.isOpen(this.awarenessChannelName)) return;
|
|
8949
9000
|
const changedClients = added.concat(updated).concat(removed);
|
|
8950
9001
|
const update = encodeAwarenessUpdate(this.awareness, changedClients);
|
|
8951
|
-
|
|
9002
|
+
this.router.send(this.awarenessChannelName, update);
|
|
8952
9003
|
};
|
|
8953
9004
|
this.awareness.on("update", this.awarenessUpdateHandler);
|
|
8954
9005
|
}
|
|
8955
9006
|
this.channelMessageHandler = ({ name, data }) => {
|
|
8956
|
-
if (name ===
|
|
8957
|
-
else if (name ===
|
|
9007
|
+
if (name === this.syncChannelName) this.handleSyncMessage(data);
|
|
9008
|
+
else if (name === this.awarenessChannelName) this.handleAwarenessMessage(data);
|
|
8958
9009
|
};
|
|
8959
9010
|
this.router.on("channelMessage", this.channelMessageHandler);
|
|
8960
9011
|
this.channelOpenHandler = ({ name }) => {
|
|
8961
|
-
if (name ===
|
|
8962
|
-
else if (name ===
|
|
8963
|
-
|
|
8964
|
-
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)) {
|
|
8965
9015
|
const update = encodeAwarenessUpdate(this.awareness, Array.from(this.awareness.getStates().keys()));
|
|
8966
|
-
|
|
9016
|
+
this.router.send(this.awarenessChannelName, update);
|
|
8967
9017
|
}
|
|
8968
9018
|
}
|
|
8969
9019
|
};
|
|
8970
9020
|
this.router.on("channelOpen", this.channelOpenHandler);
|
|
8971
|
-
if (this.router.isOpen(
|
|
8972
|
-
if (this.awareness && this.router.isOpen(
|
|
8973
|
-
const
|
|
8974
|
-
|
|
8975
|
-
const update = encodeAwarenessUpdate(this.awareness, Array.from(this.awareness.getStates().keys()));
|
|
8976
|
-
channel.send(update);
|
|
8977
|
-
}
|
|
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);
|
|
8978
9025
|
}
|
|
8979
9026
|
}
|
|
8980
9027
|
/** Stop listening and clean up handlers. */
|
|
@@ -9001,12 +9048,11 @@ var YjsDataChannel = class {
|
|
|
9001
9048
|
this.detach();
|
|
9002
9049
|
}
|
|
9003
9050
|
sendSyncStep1() {
|
|
9004
|
-
|
|
9005
|
-
if (!channel || channel.readyState !== "open") return;
|
|
9051
|
+
if (!this.router.isOpen(this.syncChannelName)) return;
|
|
9006
9052
|
const encoder = createEncoder();
|
|
9007
9053
|
writeVarUint(encoder, YJS_MSG.SYNC);
|
|
9008
9054
|
writeSyncStep1(encoder, this.document);
|
|
9009
|
-
|
|
9055
|
+
this.router.send(this.syncChannelName, toUint8Array(encoder));
|
|
9010
9056
|
}
|
|
9011
9057
|
handleSyncMessage(data) {
|
|
9012
9058
|
const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
@@ -9019,8 +9065,7 @@ var YjsDataChannel = class {
|
|
|
9019
9065
|
const responseEncoder = createEncoder();
|
|
9020
9066
|
writeVarUint(responseEncoder, YJS_MSG.SYNC);
|
|
9021
9067
|
writeUint8Array(responseEncoder, toUint8Array(encoder));
|
|
9022
|
-
|
|
9023
|
-
if (channel?.readyState === "open") channel.send(toUint8Array(responseEncoder));
|
|
9068
|
+
if (this.router.isOpen(this.syncChannelName)) this.router.send(this.syncChannelName, toUint8Array(responseEncoder));
|
|
9024
9069
|
}
|
|
9025
9070
|
if (syncMessageType === messageYjsSyncStep2) this.isSynced = true;
|
|
9026
9071
|
} else if (msgType === YJS_MSG.UPDATE) {
|
|
@@ -9270,6 +9315,136 @@ function constantTimeEqual(a, b) {
|
|
|
9270
9315
|
return result === 0;
|
|
9271
9316
|
}
|
|
9272
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
|
+
|
|
9273
9448
|
//#endregion
|
|
9274
9449
|
//#region packages/provider/src/webrtc/AbracadabraWebRTC.ts
|
|
9275
9450
|
const HAS_RTC = typeof globalThis.RTCPeerConnection !== "undefined";
|
|
@@ -9290,6 +9465,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9290
9465
|
this.peerConnections = /* @__PURE__ */ new Map();
|
|
9291
9466
|
this.yjsChannels = /* @__PURE__ */ new Map();
|
|
9292
9467
|
this.fileChannels = /* @__PURE__ */ new Map();
|
|
9468
|
+
this.e2eeChannels = /* @__PURE__ */ new Map();
|
|
9293
9469
|
this.peers = /* @__PURE__ */ new Map();
|
|
9294
9470
|
this.localPeerId = null;
|
|
9295
9471
|
this.isConnected = false;
|
|
@@ -9298,6 +9474,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9298
9474
|
this.config = {
|
|
9299
9475
|
docId: configuration.docId,
|
|
9300
9476
|
url: configuration.url,
|
|
9477
|
+
signalingUrl: configuration.signalingUrl ?? null,
|
|
9301
9478
|
token: configuration.token,
|
|
9302
9479
|
document: doc,
|
|
9303
9480
|
awareness,
|
|
@@ -9308,6 +9485,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9308
9485
|
enableAwarenessSync: configuration.enableAwarenessSync ?? !!awareness,
|
|
9309
9486
|
enableFileTransfer: configuration.enableFileTransfer ?? false,
|
|
9310
9487
|
fileChunkSize: configuration.fileChunkSize ?? 16384,
|
|
9488
|
+
e2ee: configuration.e2ee ?? null,
|
|
9311
9489
|
WebSocketPolyfill: configuration.WebSocketPolyfill
|
|
9312
9490
|
};
|
|
9313
9491
|
if (configuration.autoConnect !== false && HAS_RTC) this.connect();
|
|
@@ -9488,6 +9666,11 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9488
9666
|
}
|
|
9489
9667
|
removePeer(peerId) {
|
|
9490
9668
|
this.peers.delete(peerId);
|
|
9669
|
+
const e2ee = this.e2eeChannels.get(peerId);
|
|
9670
|
+
if (e2ee) {
|
|
9671
|
+
e2ee.destroy();
|
|
9672
|
+
this.e2eeChannels.delete(peerId);
|
|
9673
|
+
}
|
|
9491
9674
|
const yjs = this.yjsChannels.get(peerId);
|
|
9492
9675
|
if (yjs) {
|
|
9493
9676
|
yjs.destroy();
|
|
@@ -9543,12 +9726,39 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9543
9726
|
return pc;
|
|
9544
9727
|
}
|
|
9545
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) {
|
|
9546
9755
|
if (this.config.document && this.config.enableDocSync) {
|
|
9756
|
+
if (this.yjsChannels.has(peerId)) return;
|
|
9547
9757
|
const yjs = new YjsDataChannel(this.config.document, this.config.enableAwarenessSync ? this.config.awareness : null, pc.router);
|
|
9548
9758
|
yjs.attach();
|
|
9549
9759
|
this.yjsChannels.set(peerId, yjs);
|
|
9550
9760
|
}
|
|
9551
|
-
if (this.config.enableFileTransfer) {
|
|
9761
|
+
if (this.config.enableFileTransfer && !this.fileChannels.has(peerId)) {
|
|
9552
9762
|
const fc = new FileTransferChannel(pc.router, this.config.fileChunkSize);
|
|
9553
9763
|
fc.on("receiveStart", (meta) => {
|
|
9554
9764
|
this.emit("fileReceiveStart", {
|
|
@@ -9585,6 +9795,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9585
9795
|
}
|
|
9586
9796
|
async initiateConnection(peerId) {
|
|
9587
9797
|
const pc = this.createPeerConnection(peerId);
|
|
9798
|
+
if (this.config.e2ee) pc.router.createChannel(KEY_EXCHANGE_CHANNEL, { ordered: true });
|
|
9588
9799
|
pc.router.createDefaultChannels({
|
|
9589
9800
|
enableDocSync: this.config.enableDocSync,
|
|
9590
9801
|
enableAwareness: this.config.enableAwarenessSync,
|
|
@@ -9616,6 +9827,12 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9616
9827
|
}
|
|
9617
9828
|
}
|
|
9618
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
|
+
}
|
|
9619
9836
|
let base = this.config.url;
|
|
9620
9837
|
while (base.endsWith("/")) base = base.slice(0, -1);
|
|
9621
9838
|
base = base.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
|
|
@@ -9623,6 +9840,310 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9623
9840
|
}
|
|
9624
9841
|
};
|
|
9625
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
|
+
|
|
9626
10147
|
//#endregion
|
|
9627
10148
|
exports.AbracadabraBaseProvider = AbracadabraBaseProvider;
|
|
9628
10149
|
exports.AbracadabraClient = AbracadabraClient;
|
|
@@ -9633,6 +10154,7 @@ exports.AuthMessageType = AuthMessageType;
|
|
|
9633
10154
|
exports.AwarenessError = AwarenessError;
|
|
9634
10155
|
exports.BackgroundSyncManager = BackgroundSyncManager;
|
|
9635
10156
|
exports.BackgroundSyncPersistence = BackgroundSyncPersistence;
|
|
10157
|
+
exports.BroadcastChannelSync = BroadcastChannelSync;
|
|
9636
10158
|
exports.CHANNEL_NAMES = CHANNEL_NAMES;
|
|
9637
10159
|
exports.ConnectionTimeout = ConnectionTimeout;
|
|
9638
10160
|
exports.CryptoIdentityKeystore = CryptoIdentityKeystore;
|
|
@@ -9642,6 +10164,7 @@ exports.DataChannelRouter = DataChannelRouter;
|
|
|
9642
10164
|
exports.DocKeyManager = DocKeyManager;
|
|
9643
10165
|
exports.DocumentCache = DocumentCache;
|
|
9644
10166
|
exports.E2EAbracadabraProvider = E2EAbracadabraProvider;
|
|
10167
|
+
exports.E2EEChannel = E2EEChannel;
|
|
9645
10168
|
exports.E2EOfflineStore = E2EOfflineStore;
|
|
9646
10169
|
exports.EncryptedYMap = EncryptedYMap;
|
|
9647
10170
|
exports.EncryptedYText = EncryptedYText;
|
|
@@ -9651,6 +10174,8 @@ exports.FileTransferHandle = FileTransferHandle;
|
|
|
9651
10174
|
exports.Forbidden = Forbidden;
|
|
9652
10175
|
exports.HocuspocusProvider = HocuspocusProvider;
|
|
9653
10176
|
exports.HocuspocusProviderWebsocket = HocuspocusProviderWebsocket;
|
|
10177
|
+
exports.KEY_EXCHANGE_CHANNEL = KEY_EXCHANGE_CHANNEL;
|
|
10178
|
+
exports.ManualSignaling = ManualSignaling;
|
|
9654
10179
|
exports.MessageTooBig = MessageTooBig;
|
|
9655
10180
|
exports.MessageType = MessageType;
|
|
9656
10181
|
exports.OfflineStore = OfflineStore;
|