@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
|
@@ -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
|
-
|
|
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$
|
|
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$
|
|
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
|
|
8745
|
-
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
8906
|
-
else if (name ===
|
|
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 ===
|
|
8911
|
-
else if (name ===
|
|
8912
|
-
|
|
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
|
-
|
|
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(
|
|
8921
|
-
if (this.awareness && this.router.isOpen(
|
|
8922
|
-
const
|
|
8923
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|