@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.
- package/dist/abracadabra-provider.cjs +561 -37
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +558 -38
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +165 -2
- package/package.json +1 -1
- package/src/AbracadabraProvider.ts +11 -8
- package/src/AbracadabraWS.ts +1 -1
- package/src/index.ts +1 -0
- package/src/sync/BroadcastChannelSync.ts +235 -0
- package/src/types.ts +2 -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
|
@@ -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
|
-
|
|
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$
|
|
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$
|
|
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
|
|
8797
|
-
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
8958
|
-
else if (name ===
|
|
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 ===
|
|
8963
|
-
else if (name ===
|
|
8964
|
-
|
|
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
|
-
|
|
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(
|
|
8973
|
-
if (this.awareness && this.router.isOpen(
|
|
8974
|
-
const
|
|
8975
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|