@abraca/dabra 1.0.4 → 1.0.6
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
|
@@ -2935,7 +2935,7 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2935
2935
|
const childProvider = new AbracadabraProvider({
|
|
2936
2936
|
name: childId,
|
|
2937
2937
|
document: childDoc,
|
|
2938
|
-
|
|
2938
|
+
websocketProvider: this.configuration.websocketProvider,
|
|
2939
2939
|
token: this.configuration.token,
|
|
2940
2940
|
subdocLoading: this.subdocLoading,
|
|
2941
2941
|
disableOfflineStore: this.abracadabraConfig.disableOfflineStore,
|
|
@@ -2945,6 +2945,7 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2945
2945
|
docKeyManager: this.abracadabraConfig.docKeyManager,
|
|
2946
2946
|
keystore: this.abracadabraConfig.keystore
|
|
2947
2947
|
});
|
|
2948
|
+
childProvider.attach();
|
|
2948
2949
|
this.childProviders.set(childId, childProvider);
|
|
2949
2950
|
this.emit("subdocLoaded", {
|
|
2950
2951
|
childId,
|
|
@@ -6913,7 +6914,7 @@ function fromBase64url(b64) {
|
|
|
6913
6914
|
const DB_NAME = "abracadabra:identity";
|
|
6914
6915
|
const STORE_NAME = "identity";
|
|
6915
6916
|
const RECORD_KEY = "current";
|
|
6916
|
-
const HKDF_INFO$
|
|
6917
|
+
const HKDF_INFO$2 = new TextEncoder().encode("abracadabra-identity-v1");
|
|
6917
6918
|
function openDb$4() {
|
|
6918
6919
|
return new Promise((resolve, reject) => {
|
|
6919
6920
|
const req = indexedDB.open(DB_NAME, 1);
|
|
@@ -6946,7 +6947,7 @@ async function dbDelete(db) {
|
|
|
6946
6947
|
});
|
|
6947
6948
|
}
|
|
6948
6949
|
async function deriveAesKey(prfOutput, salt) {
|
|
6949
|
-
const keyBytes = hkdf(sha256, new Uint8Array(prfOutput), salt, HKDF_INFO$
|
|
6950
|
+
const keyBytes = hkdf(sha256, new Uint8Array(prfOutput), salt, HKDF_INFO$2, 32);
|
|
6950
6951
|
return crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
6951
6952
|
}
|
|
6952
6953
|
var CryptoIdentityKeystore = class {
|
|
@@ -7663,7 +7664,7 @@ var FileBlobStore = class extends EventEmitter {
|
|
|
7663
7664
|
* Manages AES-256-GCM document keys for CSE and E2E encrypted documents.
|
|
7664
7665
|
* Keys are wrapped per-user using X25519 ECDH + HKDF-SHA256 + AES-256-GCM.
|
|
7665
7666
|
*/
|
|
7666
|
-
const HKDF_INFO = new TextEncoder().encode("abracadabra-dockey-v1");
|
|
7667
|
+
const HKDF_INFO$1 = new TextEncoder().encode("abracadabra-dockey-v1");
|
|
7667
7668
|
function fromBase64$1(b64) {
|
|
7668
7669
|
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
7669
7670
|
}
|
|
@@ -7707,7 +7708,7 @@ var DocKeyManager = class {
|
|
|
7707
7708
|
async wrapKeyForRecipient(docKey, recipientX25519PubKey, docId) {
|
|
7708
7709
|
const ephemeralPriv = crypto.getRandomValues(new Uint8Array(32));
|
|
7709
7710
|
const ephemeralPub = x25519.getPublicKey(ephemeralPriv);
|
|
7710
|
-
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);
|
|
7711
7712
|
const wrapKey = await crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt"]);
|
|
7712
7713
|
const rawDocKey = await crypto.subtle.exportKey("raw", docKey);
|
|
7713
7714
|
const nonce = crypto.getRandomValues(new Uint8Array(12));
|
|
@@ -7725,7 +7726,7 @@ var DocKeyManager = class {
|
|
|
7725
7726
|
const ephemeralPub = wrapped.slice(0, 32);
|
|
7726
7727
|
const nonce = wrapped.slice(32, 44);
|
|
7727
7728
|
const ciphertext = wrapped.slice(44);
|
|
7728
|
-
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);
|
|
7729
7730
|
const wrapKey = await crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["decrypt"]);
|
|
7730
7731
|
const rawDocKey = await crypto.subtle.decrypt({
|
|
7731
7732
|
name: "AES-GCM",
|
|
@@ -8695,15 +8696,23 @@ const SHA256_BYTES = 32;
|
|
|
8695
8696
|
|
|
8696
8697
|
//#endregion
|
|
8697
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";
|
|
8698
8701
|
var DataChannelRouter = class extends EventEmitter {
|
|
8699
8702
|
constructor(connection) {
|
|
8700
8703
|
super();
|
|
8701
8704
|
this.connection = connection;
|
|
8702
8705
|
this.channels = /* @__PURE__ */ new Map();
|
|
8706
|
+
this.encryptor = null;
|
|
8707
|
+
this.plaintextChannels = new Set([KEY_EXCHANGE_CHANNEL]);
|
|
8703
8708
|
this.connection.ondatachannel = (event) => {
|
|
8704
8709
|
this.registerChannel(event.channel);
|
|
8705
8710
|
};
|
|
8706
8711
|
}
|
|
8712
|
+
/** Attach an E2EE encryptor. All channels (except key-exchange) will be encrypted. */
|
|
8713
|
+
setEncryptor(encryptor) {
|
|
8714
|
+
this.encryptor = encryptor;
|
|
8715
|
+
}
|
|
8707
8716
|
/** Create a named data channel (initiator side). */
|
|
8708
8717
|
createChannel(name, options) {
|
|
8709
8718
|
const channel = this.connection.createDataChannel(name, options);
|
|
@@ -8719,12 +8728,35 @@ var DataChannelRouter = class extends EventEmitter {
|
|
|
8719
8728
|
});
|
|
8720
8729
|
if (opts.enableFileTransfer) this.createChannel(CHANNEL_NAMES.FILE_TRANSFER, { ordered: true });
|
|
8721
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
|
+
}
|
|
8722
8742
|
getChannel(name) {
|
|
8723
8743
|
return this.channels.get(name) ?? null;
|
|
8724
8744
|
}
|
|
8725
8745
|
isOpen(name) {
|
|
8726
8746
|
return this.channels.get(name)?.readyState === "open";
|
|
8727
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
|
+
}
|
|
8728
8760
|
registerChannel(channel) {
|
|
8729
8761
|
channel.binaryType = "arraybuffer";
|
|
8730
8762
|
this.channels.set(channel.label, channel);
|
|
@@ -8738,10 +8770,22 @@ var DataChannelRouter = class extends EventEmitter {
|
|
|
8738
8770
|
this.emit("channelClose", { name: channel.label });
|
|
8739
8771
|
this.channels.delete(channel.label);
|
|
8740
8772
|
};
|
|
8741
|
-
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
|
+
}
|
|
8742
8786
|
this.emit("channelMessage", {
|
|
8743
|
-
name
|
|
8744
|
-
data
|
|
8787
|
+
name,
|
|
8788
|
+
data
|
|
8745
8789
|
});
|
|
8746
8790
|
};
|
|
8747
8791
|
channel.onerror = (event) => {
|
|
@@ -8869,7 +8913,13 @@ var PeerConnection = class extends EventEmitter {
|
|
|
8869
8913
|
* prevent echo loops with the server-based provider.
|
|
8870
8914
|
*/
|
|
8871
8915
|
var YjsDataChannel = class {
|
|
8872
|
-
|
|
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) {
|
|
8873
8923
|
this.document = document;
|
|
8874
8924
|
this.awareness = awareness;
|
|
8875
8925
|
this.router = router;
|
|
@@ -8877,52 +8927,49 @@ var YjsDataChannel = class {
|
|
|
8877
8927
|
this.awarenessUpdateHandler = null;
|
|
8878
8928
|
this.channelOpenHandler = null;
|
|
8879
8929
|
this.channelMessageHandler = null;
|
|
8930
|
+
const prefix = channelPrefix ?? "";
|
|
8931
|
+
this.syncChannelName = `${prefix}${CHANNEL_NAMES.YJS_SYNC}`;
|
|
8932
|
+
this.awarenessChannelName = `${prefix}${CHANNEL_NAMES.AWARENESS}`;
|
|
8880
8933
|
}
|
|
8881
8934
|
/** Start listening for Y.js updates and data channel messages. */
|
|
8882
8935
|
attach() {
|
|
8883
8936
|
this.docUpdateHandler = (update, origin) => {
|
|
8884
8937
|
if (origin === this) return;
|
|
8885
|
-
|
|
8886
|
-
if (!channel || channel.readyState !== "open") return;
|
|
8938
|
+
if (!this.router.isOpen(this.syncChannelName)) return;
|
|
8887
8939
|
const encoder = createEncoder();
|
|
8888
8940
|
writeVarUint(encoder, YJS_MSG.UPDATE);
|
|
8889
8941
|
writeVarUint8Array(encoder, update);
|
|
8890
|
-
|
|
8942
|
+
this.router.send(this.syncChannelName, toUint8Array(encoder));
|
|
8891
8943
|
};
|
|
8892
8944
|
this.document.on("update", this.docUpdateHandler);
|
|
8893
8945
|
if (this.awareness) {
|
|
8894
8946
|
this.awarenessUpdateHandler = ({ added, updated, removed }, _origin) => {
|
|
8895
|
-
|
|
8896
|
-
if (!channel || channel.readyState !== "open") return;
|
|
8947
|
+
if (!this.router.isOpen(this.awarenessChannelName)) return;
|
|
8897
8948
|
const changedClients = added.concat(updated).concat(removed);
|
|
8898
8949
|
const update = encodeAwarenessUpdate(this.awareness, changedClients);
|
|
8899
|
-
|
|
8950
|
+
this.router.send(this.awarenessChannelName, update);
|
|
8900
8951
|
};
|
|
8901
8952
|
this.awareness.on("update", this.awarenessUpdateHandler);
|
|
8902
8953
|
}
|
|
8903
8954
|
this.channelMessageHandler = ({ name, data }) => {
|
|
8904
|
-
if (name ===
|
|
8905
|
-
else if (name ===
|
|
8955
|
+
if (name === this.syncChannelName) this.handleSyncMessage(data);
|
|
8956
|
+
else if (name === this.awarenessChannelName) this.handleAwarenessMessage(data);
|
|
8906
8957
|
};
|
|
8907
8958
|
this.router.on("channelMessage", this.channelMessageHandler);
|
|
8908
8959
|
this.channelOpenHandler = ({ name }) => {
|
|
8909
|
-
if (name ===
|
|
8910
|
-
else if (name ===
|
|
8911
|
-
|
|
8912
|
-
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)) {
|
|
8913
8963
|
const update = encodeAwarenessUpdate(this.awareness, Array.from(this.awareness.getStates().keys()));
|
|
8914
|
-
|
|
8964
|
+
this.router.send(this.awarenessChannelName, update);
|
|
8915
8965
|
}
|
|
8916
8966
|
}
|
|
8917
8967
|
};
|
|
8918
8968
|
this.router.on("channelOpen", this.channelOpenHandler);
|
|
8919
|
-
if (this.router.isOpen(
|
|
8920
|
-
if (this.awareness && this.router.isOpen(
|
|
8921
|
-
const
|
|
8922
|
-
|
|
8923
|
-
const update = encodeAwarenessUpdate(this.awareness, Array.from(this.awareness.getStates().keys()));
|
|
8924
|
-
channel.send(update);
|
|
8925
|
-
}
|
|
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);
|
|
8926
8973
|
}
|
|
8927
8974
|
}
|
|
8928
8975
|
/** Stop listening and clean up handlers. */
|
|
@@ -8949,12 +8996,11 @@ var YjsDataChannel = class {
|
|
|
8949
8996
|
this.detach();
|
|
8950
8997
|
}
|
|
8951
8998
|
sendSyncStep1() {
|
|
8952
|
-
|
|
8953
|
-
if (!channel || channel.readyState !== "open") return;
|
|
8999
|
+
if (!this.router.isOpen(this.syncChannelName)) return;
|
|
8954
9000
|
const encoder = createEncoder();
|
|
8955
9001
|
writeVarUint(encoder, YJS_MSG.SYNC);
|
|
8956
9002
|
writeSyncStep1(encoder, this.document);
|
|
8957
|
-
|
|
9003
|
+
this.router.send(this.syncChannelName, toUint8Array(encoder));
|
|
8958
9004
|
}
|
|
8959
9005
|
handleSyncMessage(data) {
|
|
8960
9006
|
const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
@@ -8967,8 +9013,7 @@ var YjsDataChannel = class {
|
|
|
8967
9013
|
const responseEncoder = createEncoder();
|
|
8968
9014
|
writeVarUint(responseEncoder, YJS_MSG.SYNC);
|
|
8969
9015
|
writeUint8Array(responseEncoder, toUint8Array(encoder));
|
|
8970
|
-
|
|
8971
|
-
if (channel?.readyState === "open") channel.send(toUint8Array(responseEncoder));
|
|
9016
|
+
if (this.router.isOpen(this.syncChannelName)) this.router.send(this.syncChannelName, toUint8Array(responseEncoder));
|
|
8972
9017
|
}
|
|
8973
9018
|
if (syncMessageType === messageYjsSyncStep2) this.isSynced = true;
|
|
8974
9019
|
} else if (msgType === YJS_MSG.UPDATE) {
|
|
@@ -9218,6 +9263,136 @@ function constantTimeEqual(a, b) {
|
|
|
9218
9263
|
return result === 0;
|
|
9219
9264
|
}
|
|
9220
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
|
+
|
|
9221
9396
|
//#endregion
|
|
9222
9397
|
//#region packages/provider/src/webrtc/AbracadabraWebRTC.ts
|
|
9223
9398
|
const HAS_RTC = typeof globalThis.RTCPeerConnection !== "undefined";
|
|
@@ -9238,6 +9413,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9238
9413
|
this.peerConnections = /* @__PURE__ */ new Map();
|
|
9239
9414
|
this.yjsChannels = /* @__PURE__ */ new Map();
|
|
9240
9415
|
this.fileChannels = /* @__PURE__ */ new Map();
|
|
9416
|
+
this.e2eeChannels = /* @__PURE__ */ new Map();
|
|
9241
9417
|
this.peers = /* @__PURE__ */ new Map();
|
|
9242
9418
|
this.localPeerId = null;
|
|
9243
9419
|
this.isConnected = false;
|
|
@@ -9246,6 +9422,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9246
9422
|
this.config = {
|
|
9247
9423
|
docId: configuration.docId,
|
|
9248
9424
|
url: configuration.url,
|
|
9425
|
+
signalingUrl: configuration.signalingUrl ?? null,
|
|
9249
9426
|
token: configuration.token,
|
|
9250
9427
|
document: doc,
|
|
9251
9428
|
awareness,
|
|
@@ -9256,6 +9433,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9256
9433
|
enableAwarenessSync: configuration.enableAwarenessSync ?? !!awareness,
|
|
9257
9434
|
enableFileTransfer: configuration.enableFileTransfer ?? false,
|
|
9258
9435
|
fileChunkSize: configuration.fileChunkSize ?? 16384,
|
|
9436
|
+
e2ee: configuration.e2ee ?? null,
|
|
9259
9437
|
WebSocketPolyfill: configuration.WebSocketPolyfill
|
|
9260
9438
|
};
|
|
9261
9439
|
if (configuration.autoConnect !== false && HAS_RTC) this.connect();
|
|
@@ -9436,6 +9614,11 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9436
9614
|
}
|
|
9437
9615
|
removePeer(peerId) {
|
|
9438
9616
|
this.peers.delete(peerId);
|
|
9617
|
+
const e2ee = this.e2eeChannels.get(peerId);
|
|
9618
|
+
if (e2ee) {
|
|
9619
|
+
e2ee.destroy();
|
|
9620
|
+
this.e2eeChannels.delete(peerId);
|
|
9621
|
+
}
|
|
9439
9622
|
const yjs = this.yjsChannels.get(peerId);
|
|
9440
9623
|
if (yjs) {
|
|
9441
9624
|
yjs.destroy();
|
|
@@ -9491,12 +9674,39 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9491
9674
|
return pc;
|
|
9492
9675
|
}
|
|
9493
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) {
|
|
9494
9703
|
if (this.config.document && this.config.enableDocSync) {
|
|
9704
|
+
if (this.yjsChannels.has(peerId)) return;
|
|
9495
9705
|
const yjs = new YjsDataChannel(this.config.document, this.config.enableAwarenessSync ? this.config.awareness : null, pc.router);
|
|
9496
9706
|
yjs.attach();
|
|
9497
9707
|
this.yjsChannels.set(peerId, yjs);
|
|
9498
9708
|
}
|
|
9499
|
-
if (this.config.enableFileTransfer) {
|
|
9709
|
+
if (this.config.enableFileTransfer && !this.fileChannels.has(peerId)) {
|
|
9500
9710
|
const fc = new FileTransferChannel(pc.router, this.config.fileChunkSize);
|
|
9501
9711
|
fc.on("receiveStart", (meta) => {
|
|
9502
9712
|
this.emit("fileReceiveStart", {
|
|
@@ -9533,6 +9743,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9533
9743
|
}
|
|
9534
9744
|
async initiateConnection(peerId) {
|
|
9535
9745
|
const pc = this.createPeerConnection(peerId);
|
|
9746
|
+
if (this.config.e2ee) pc.router.createChannel(KEY_EXCHANGE_CHANNEL, { ordered: true });
|
|
9536
9747
|
pc.router.createDefaultChannels({
|
|
9537
9748
|
enableDocSync: this.config.enableDocSync,
|
|
9538
9749
|
enableAwareness: this.config.enableAwarenessSync,
|
|
@@ -9564,6 +9775,12 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9564
9775
|
}
|
|
9565
9776
|
}
|
|
9566
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
|
+
}
|
|
9567
9784
|
let base = this.config.url;
|
|
9568
9785
|
while (base.endsWith("/")) base = base.slice(0, -1);
|
|
9569
9786
|
base = base.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
|
|
@@ -9572,5 +9789,309 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9572
9789
|
};
|
|
9573
9790
|
|
|
9574
9791
|
//#endregion
|
|
9575
|
-
|
|
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 };
|
|
9576
10097
|
//# sourceMappingURL=abracadabra-provider.esm.js.map
|