@abraca/dabra 1.0.21 → 1.0.23

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.
@@ -2792,7 +2792,7 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
2792
2792
  this._client = client;
2793
2793
  this.abracadabraConfig = configuration;
2794
2794
  this.subdocLoading = configuration.subdocLoading ?? "lazy";
2795
- const serverOrigin = AbracadabraProvider.deriveServerOrigin(configuration, client);
2795
+ const serverOrigin = configuration.serverAgnostic ? void 0 : AbracadabraProvider.deriveServerOrigin(configuration, client);
2796
2796
  this.offlineStore = configuration.disableOfflineStore ? null : new OfflineStore(configuration.name, serverOrigin);
2797
2797
  this.on("subdocRegistered", configuration.onSubdocRegistered ?? (() => null));
2798
2798
  this.on("subdocLoaded", configuration.onSubdocLoaded ?? (() => null));
@@ -3222,6 +3222,18 @@ var AbracadabraClient = class {
3222
3222
  auth: false
3223
3223
  });
3224
3224
  }
3225
+ /**
3226
+ * Fetch a short-lived anonymous pairing token for WebRTC signaling.
3227
+ * No authentication required. The token only grants access to `__pairing_*` rooms.
3228
+ */
3229
+ static async getPairingToken(serverUrl) {
3230
+ let base = serverUrl;
3231
+ while (base.endsWith("/")) base = base.slice(0, -1);
3232
+ const resp = await fetch(`${base}/auth/pairing-token`, { method: "POST" });
3233
+ if (!resp.ok) throw new Error(`Failed to fetch pairing token: ${resp.status}`);
3234
+ const { token } = await resp.json();
3235
+ return token;
3236
+ }
3225
3237
  /** Get encryption info for a document. */
3226
3238
  async getDocEncryption(docId) {
3227
3239
  return this.request("GET", `/docs/${encodeURIComponent(docId)}/encryption`);
@@ -8858,8 +8870,8 @@ var SignalingSocket = class extends EventEmitter {
8858
8870
  break;
8859
8871
  case "joined":
8860
8872
  this.emit("joined", {
8861
- peerId: msg.peer_id,
8862
- userId: msg.user_id,
8873
+ peer_id: msg.peer_id,
8874
+ user_id: msg.user_id,
8863
8875
  muted: msg.muted,
8864
8876
  video: msg.video,
8865
8877
  screen: msg.screen,
@@ -10297,6 +10309,7 @@ var ManualSignaling = class extends EventEmitter {
10297
10309
  const CODE_CHARSET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
10298
10310
  const CODE_LENGTH = 6;
10299
10311
  const PAIRING_TIMEOUT_MS = 300 * 1e3;
10312
+ const SIGNALING_CONNECT_TIMEOUT_MS = 5e3;
10300
10313
  function generatePairingCode() {
10301
10314
  const bytes = crypto.getRandomValues(new Uint8Array(CODE_LENGTH));
10302
10315
  return Array.from(bytes).map((b) => CODE_CHARSET[b % 32]).join("");
@@ -10314,6 +10327,7 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
10314
10327
  this._destroyed = false;
10315
10328
  this._pendingRequest = null;
10316
10329
  this._connectedPeerId = null;
10330
+ this._usingFallback = false;
10317
10331
  this.role = role;
10318
10332
  this.pairingCode = pairingCode;
10319
10333
  }
@@ -10468,12 +10482,41 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
10468
10482
  }
10469
10483
  this.removeAllListeners();
10470
10484
  }
10485
+ async resolveToken(serverUrl) {
10486
+ if (this.config.token && serverUrl === this.config.serverUrl) return this.config.token;
10487
+ let base = serverUrl ?? this.config.serverUrl;
10488
+ while (base.endsWith("/")) base = base.slice(0, -1);
10489
+ const resp = await fetch(`${base}/auth/pairing-token`, { method: "POST" });
10490
+ if (!resp.ok) throw new Error(`Failed to fetch pairing token: ${resp.status} ${resp.statusText}`);
10491
+ const { token } = await resp.json();
10492
+ return token;
10493
+ }
10471
10494
  start() {
10495
+ this.connectToServer(this.config.serverUrl);
10496
+ this.timeoutHandle = setTimeout(() => {
10497
+ if (!this._destroyed) {
10498
+ this.emit("error", /* @__PURE__ */ new Error("Pairing timed out"));
10499
+ this.destroy();
10500
+ }
10501
+ }, PAIRING_TIMEOUT_MS);
10502
+ }
10503
+ connectToServer(serverUrl, signalingUrl) {
10504
+ const roomId = codeToRoomId(this.pairingCode);
10505
+ const tokenPromise = this.resolveToken(serverUrl);
10506
+ const tokenFactory = async () => {
10507
+ const t = await tokenPromise;
10508
+ if (typeof t === "function") return await t();
10509
+ return t;
10510
+ };
10511
+ if (!this.config.iceServers) new AbracadabraClient({ url: serverUrl }).getIceServers().then((servers) => {
10512
+ if (servers.length > 0) this._resolvedIceServers = servers;
10513
+ });
10472
10514
  this.webrtc = new AbracadabraWebRTC({
10473
- docId: codeToRoomId(this.pairingCode),
10474
- url: this.config.serverUrl,
10475
- token: this.config.token,
10476
- iceServers: this.config.iceServers,
10515
+ docId: roomId,
10516
+ url: serverUrl,
10517
+ signalingUrl: signalingUrl ?? void 0,
10518
+ token: tokenFactory,
10519
+ iceServers: this.config.iceServers ?? this._resolvedIceServers,
10477
10520
  e2ee: this.config.e2ee,
10478
10521
  enableDocSync: false,
10479
10522
  enableAwarenessSync: false,
@@ -10481,6 +10524,10 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
10481
10524
  autoConnect: false,
10482
10525
  WebSocketPolyfill: this.config.WebSocketPolyfill
10483
10526
  });
10527
+ let connected = false;
10528
+ this.webrtc.on("connected", () => {
10529
+ connected = true;
10530
+ });
10484
10531
  this.webrtc.on("e2eeEstablished", ({ peerId }) => {
10485
10532
  this._connectedPeerId = peerId;
10486
10533
  this.emit("connected");
@@ -10494,14 +10541,25 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
10494
10541
  this.webrtc.on("signalingError", (err) => {
10495
10542
  this.emit("error", /* @__PURE__ */ new Error(`Signaling: ${err.message}`));
10496
10543
  });
10497
- this.timeoutHandle = setTimeout(() => {
10498
- if (!this._destroyed) {
10499
- this.emit("error", /* @__PURE__ */ new Error("Pairing timed out"));
10500
- this.destroy();
10501
- }
10502
- }, PAIRING_TIMEOUT_MS);
10544
+ if (this.config.fallbackSignalingUrl && !signalingUrl) {
10545
+ const fallbackTimer = setTimeout(() => {
10546
+ if (this._destroyed || connected) return;
10547
+ if (this.webrtc) {
10548
+ this.webrtc.destroy();
10549
+ this.webrtc = null;
10550
+ }
10551
+ this._usingFallback = true;
10552
+ this.emit("fallback", { url: this.config.fallbackSignalingUrl });
10553
+ this.connectToServer(this.config.fallbackSignalingUrl);
10554
+ }, SIGNALING_CONNECT_TIMEOUT_MS);
10555
+ this.webrtc.on("connected", () => clearTimeout(fallbackTimer));
10556
+ }
10503
10557
  this.webrtc.connect();
10504
10558
  }
10559
+ /** Whether the connection fell back to the fallback signaling server. */
10560
+ get usingFallback() {
10561
+ return this._usingFallback;
10562
+ }
10505
10563
  sendMessage(msg) {
10506
10564
  if (!this.webrtc || !this._connectedPeerId) return;
10507
10565
  this.webrtc.sendCustomMessage(this._connectedPeerId, JSON.stringify(msg));
@@ -10696,5 +10754,319 @@ var BroadcastChannelSync = class BroadcastChannelSync extends EventEmitter {
10696
10754
  };
10697
10755
 
10698
10756
  //#endregion
10699
- export { AbracadabraBaseProvider, AbracadabraClient, AbracadabraProvider, AbracadabraWS, AbracadabraWebRTC, AuthMessageType, AwarenessError, BackgroundSyncManager, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ConnectionTimeout, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, DevicePairingChannel, 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 };
10757
+ //#region packages/provider/src/IdentityDoc.ts
10758
+ /**
10759
+ * Derives a deterministic UUID from an Ed25519 account-level public key.
10760
+ *
10761
+ * The result is a valid UUID v5-style string that any device sharing the
10762
+ * same identity can independently compute. The `abracadabra:identity:`
10763
+ * prefix prevents collisions with randomly generated doc UUIDs.
10764
+ *
10765
+ * @param publicKeyB64 Base64url-encoded Ed25519 public key (32 bytes).
10766
+ */
10767
+ function deriveIdentityDocId(publicKeyB64) {
10768
+ const hash = sha256(new TextEncoder().encode(`abracadabra:identity:${publicKeyB64}`));
10769
+ const bytes = new Uint8Array(hash.buffer, hash.byteOffset, 16);
10770
+ bytes[6] = bytes[6] & 15 | 80;
10771
+ bytes[8] = bytes[8] & 63 | 128;
10772
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
10773
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
10774
+ }
10775
+ /**
10776
+ * Manages a Y.Doc dedicated to user identity that syncs across trusted
10777
+ * targets only: a designated sync server, a local Tauri server, and/or
10778
+ * WebRTC P2P.
10779
+ *
10780
+ * The Y.Doc contains cross-device settings (profile, servers, spaces,
10781
+ * plugins, preferences). Each sync target gets its own
10782
+ * AbracadabraProvider sharing the same Y.Doc, with `serverAgnostic: true`
10783
+ * so they all use one IndexedDB store.
10784
+ */
10785
+ var IdentityDocProvider = class extends EventEmitter {
10786
+ constructor(configuration) {
10787
+ super();
10788
+ this.providers = /* @__PURE__ */ new Map();
10789
+ this.websockets = /* @__PURE__ */ new Map();
10790
+ this.webrtc = null;
10791
+ this._destroyed = false;
10792
+ this.config = configuration;
10793
+ this.docId = deriveIdentityDocId(configuration.publicKey);
10794
+ this.document = new Y.Doc({ guid: this.docId });
10795
+ if (configuration.autoConnect !== false) this.connect();
10796
+ }
10797
+ connect() {
10798
+ if (this._destroyed) return;
10799
+ const targets = [];
10800
+ if (this.config.localServerUrl) targets.push({
10801
+ url: this.config.localServerUrl,
10802
+ key: "local"
10803
+ });
10804
+ if (this.config.syncServerUrl) targets.push({
10805
+ url: this.config.syncServerUrl,
10806
+ key: "sync"
10807
+ });
10808
+ for (const { url, key } of targets) {
10809
+ if (this.providers.has(key)) continue;
10810
+ this.connectToServer(key, url);
10811
+ }
10812
+ if (this.config.webrtc && !this.webrtc) this._connectWebRTC();
10813
+ }
10814
+ connectToServer(key, serverUrl) {
10815
+ if (this._destroyed) return;
10816
+ const existingProvider = this.providers.get(key);
10817
+ if (existingProvider) {
10818
+ existingProvider.destroy();
10819
+ this.providers.delete(key);
10820
+ }
10821
+ const existingWs = this.websockets.get(key);
10822
+ if (existingWs) {
10823
+ existingWs.destroy();
10824
+ this.websockets.delete(key);
10825
+ }
10826
+ const token = this.config.tokens?.[serverUrl] ?? this.config.token ?? "";
10827
+ const ws = new AbracadabraWS({
10828
+ url: serverUrl.replace(/^http/, "ws").replace(/\/$/, "").concat("/ws"),
10829
+ WebSocketPolyfill: void 0
10830
+ });
10831
+ this.websockets.set(key, ws);
10832
+ const providerConfig = {
10833
+ name: this.docId,
10834
+ document: this.document,
10835
+ websocketProvider: ws,
10836
+ serverAgnostic: true,
10837
+ disableOfflineStore: key !== "local" && key !== "sync" ? true : this.config.disableOfflineStore ?? false,
10838
+ ...this.config.providerDefaults
10839
+ };
10840
+ if (this.config.cryptoIdentity && this.config.signChallenge) {
10841
+ providerConfig.cryptoIdentity = this.config.cryptoIdentity;
10842
+ providerConfig.signChallenge = this.config.signChallenge;
10843
+ } else providerConfig.token = token;
10844
+ const provider = new AbracadabraProvider(providerConfig);
10845
+ provider.on("synced", () => this.emit("synced", { server: key }));
10846
+ provider.on("status", (data) => this.emit("status", {
10847
+ server: key,
10848
+ ...data
10849
+ }));
10850
+ this.providers.set(key, provider);
10851
+ }
10852
+ _connectWebRTC() {
10853
+ const rtcConfig = this.config.webrtc;
10854
+ if (!rtcConfig) return;
10855
+ this.webrtc = new AbracadabraWebRTC({
10856
+ docId: this.docId,
10857
+ url: rtcConfig.signalingServerUrl,
10858
+ token: rtcConfig.token,
10859
+ document: this.document,
10860
+ enableDocSync: true,
10861
+ enableAwarenessSync: false,
10862
+ enableFileTransfer: false,
10863
+ e2ee: rtcConfig.e2ee,
10864
+ iceServers: rtcConfig.iceServers
10865
+ });
10866
+ }
10867
+ get profileMap() {
10868
+ return this.document.getMap("profile");
10869
+ }
10870
+ get serversMap() {
10871
+ return this.document.getMap("servers");
10872
+ }
10873
+ get spacesArray() {
10874
+ return this.document.getArray("spaces");
10875
+ }
10876
+ get pluginsMap() {
10877
+ return this.document.getMap("plugins");
10878
+ }
10879
+ get preferencesMap() {
10880
+ return this.document.getMap("preferences");
10881
+ }
10882
+ getProfile() {
10883
+ const m = this.profileMap;
10884
+ return {
10885
+ username: m.get("username"),
10886
+ displayName: m.get("displayName"),
10887
+ colorName: m.get("colorName"),
10888
+ neutralColorName: m.get("neutralColorName"),
10889
+ locale: m.get("locale"),
10890
+ avatarUrl: m.get("avatarUrl")
10891
+ };
10892
+ }
10893
+ setProfile(profile) {
10894
+ const m = this.profileMap;
10895
+ this.document.transact(() => {
10896
+ for (const [key, value] of Object.entries(profile)) if (value !== void 0) m.set(key, value);
10897
+ });
10898
+ }
10899
+ getServer(url) {
10900
+ const entry = this.serversMap.get(url);
10901
+ if (!entry) return void 0;
10902
+ return {
10903
+ label: entry.get("label") ?? url,
10904
+ hubDocId: entry.get("hubDocId"),
10905
+ entryDocId: entry.get("entryDocId"),
10906
+ defaultRole: entry.get("defaultRole"),
10907
+ spacesEnabled: entry.get("spacesEnabled"),
10908
+ addedAt: entry.get("addedAt") ?? 0
10909
+ };
10910
+ }
10911
+ setServer(url, entry) {
10912
+ const m = this.serversMap;
10913
+ this.document.transact(() => {
10914
+ let yEntry = m.get(url);
10915
+ if (!yEntry) {
10916
+ yEntry = new Y.Map();
10917
+ m.set(url, yEntry);
10918
+ }
10919
+ for (const [key, value] of Object.entries(entry)) if (value !== void 0) yEntry.set(key, value);
10920
+ });
10921
+ }
10922
+ removeServer(url) {
10923
+ this.serversMap.delete(url);
10924
+ }
10925
+ getServers() {
10926
+ const result = /* @__PURE__ */ new Map();
10927
+ this.serversMap.forEach((entry, url) => {
10928
+ result.set(url, {
10929
+ label: entry.get("label") ?? url,
10930
+ hubDocId: entry.get("hubDocId"),
10931
+ entryDocId: entry.get("entryDocId"),
10932
+ defaultRole: entry.get("defaultRole"),
10933
+ spacesEnabled: entry.get("spacesEnabled"),
10934
+ addedAt: entry.get("addedAt") ?? 0
10935
+ });
10936
+ });
10937
+ return result;
10938
+ }
10939
+ getSpaces() {
10940
+ const result = [];
10941
+ this.spacesArray.forEach((yMap) => {
10942
+ result.push({
10943
+ id: yMap.get("id"),
10944
+ name: yMap.get("name"),
10945
+ type: yMap.get("type") ?? "remote",
10946
+ serverUrl: yMap.get("serverUrl") ?? null,
10947
+ docId: yMap.get("docId"),
10948
+ remoteSpaceId: yMap.get("remoteSpaceId"),
10949
+ visibility: yMap.get("visibility") ?? "private",
10950
+ isHub: yMap.get("isHub") ?? false,
10951
+ order: yMap.get("order") ?? 0,
10952
+ lastSyncedAt: yMap.get("lastSyncedAt"),
10953
+ createdAt: yMap.get("createdAt") ?? 0
10954
+ });
10955
+ });
10956
+ return result;
10957
+ }
10958
+ addSpace(space) {
10959
+ const yMap = new Y.Map();
10960
+ this.document.transact(() => {
10961
+ for (const [key, value] of Object.entries(space)) if (value !== void 0) yMap.set(key, value);
10962
+ this.spacesArray.push([yMap]);
10963
+ });
10964
+ }
10965
+ removeSpace(spaceId) {
10966
+ const arr = this.spacesArray;
10967
+ for (let i = 0; i < arr.length; i++) if (arr.get(i).get("id") === spaceId) {
10968
+ arr.delete(i, 1);
10969
+ return;
10970
+ }
10971
+ }
10972
+ updateSpace(spaceId, updates) {
10973
+ const arr = this.spacesArray;
10974
+ for (let i = 0; i < arr.length; i++) {
10975
+ const yMap = arr.get(i);
10976
+ if (yMap.get("id") === spaceId) {
10977
+ this.document.transact(() => {
10978
+ for (const [key, value] of Object.entries(updates)) if (value !== void 0) yMap.set(key, value);
10979
+ });
10980
+ return;
10981
+ }
10982
+ }
10983
+ }
10984
+ getExternalPlugins() {
10985
+ let arr = this.pluginsMap.get("external");
10986
+ if (!arr) {
10987
+ arr = new Y.Array();
10988
+ this.pluginsMap.set("external", arr);
10989
+ }
10990
+ return arr;
10991
+ }
10992
+ getDisabledBuiltins() {
10993
+ let arr = this.pluginsMap.get("disabledBuiltins");
10994
+ if (!arr) {
10995
+ arr = new Y.Array();
10996
+ this.pluginsMap.set("disabledBuiltins", arr);
10997
+ }
10998
+ return arr;
10999
+ }
11000
+ getPreference(key) {
11001
+ return this.preferencesMap.get(key);
11002
+ }
11003
+ setPreference(key, value) {
11004
+ this.preferencesMap.set(key, value);
11005
+ }
11006
+ /**
11007
+ * Observe deep changes on a specific top-level map.
11008
+ * Returns an unsubscribe function.
11009
+ */
11010
+ observe(mapName, callback) {
11011
+ const map = this.document.getMap(mapName);
11012
+ map.observeDeep(callback);
11013
+ return () => map.unobserveDeep(callback);
11014
+ }
11015
+ /**
11016
+ * Observe changes to the spaces array.
11017
+ * Returns an unsubscribe function.
11018
+ */
11019
+ observeSpaces(callback) {
11020
+ const arr = this.spacesArray;
11021
+ arr.observe(callback);
11022
+ return () => arr.unobserve(callback);
11023
+ }
11024
+ /**
11025
+ * Returns true if the identity doc has no profile data yet (first use).
11026
+ * Call this to decide whether to run migration from localStorage.
11027
+ */
11028
+ isEmpty() {
11029
+ return this.profileMap.size === 0 && this.serversMap.size === 0;
11030
+ }
11031
+ /**
11032
+ * Update the sync server URL at runtime (e.g. when user changes their
11033
+ * designated sync server in settings).
11034
+ */
11035
+ setSyncServer(url) {
11036
+ const existing = this.providers.get("sync");
11037
+ if (existing) {
11038
+ existing.destroy();
11039
+ this.providers.delete("sync");
11040
+ }
11041
+ const existingWs = this.websockets.get("sync");
11042
+ if (existingWs) {
11043
+ existingWs.destroy();
11044
+ this.websockets.delete("sync");
11045
+ }
11046
+ if (url) {
11047
+ this.config = {
11048
+ ...this.config,
11049
+ syncServerUrl: url
11050
+ };
11051
+ this.connectToServer("sync", url);
11052
+ }
11053
+ }
11054
+ getProvider(key) {
11055
+ return this.providers.get(key);
11056
+ }
11057
+ destroy() {
11058
+ if (this._destroyed) return;
11059
+ this._destroyed = true;
11060
+ for (const [, provider] of this.providers) provider.destroy();
11061
+ for (const [, ws] of this.websockets) ws.destroy();
11062
+ if (this.webrtc) this.webrtc.disconnect?.();
11063
+ this.providers.clear();
11064
+ this.websockets.clear();
11065
+ this.webrtc = null;
11066
+ this.document.destroy();
11067
+ }
11068
+ };
11069
+
11070
+ //#endregion
11071
+ export { AbracadabraBaseProvider, AbracadabraClient, AbracadabraProvider, AbracadabraWS, AbracadabraWebRTC, AuthMessageType, AwarenessError, BackgroundSyncManager, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ConnectionTimeout, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, DevicePairingChannel, DocKeyManager, DocumentCache, E2EAbracadabraProvider, E2EEChannel, E2EOfflineStore, EncryptedYMap, EncryptedYText, FileBlobStore, FileTransferChannel, FileTransferHandle, Forbidden, HocuspocusProvider, HocuspocusProviderWebsocket, IdentityDocProvider, KEY_EXCHANGE_CHANNEL, ManualSignaling, MessageTooBig, MessageType, OfflineStore, PeerConnection, ResetConnection, SearchIndex, SignalingSocket, SubdocMessage, Unauthorized, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, decryptField, deriveIdentityDocId, encryptField, makeEncryptedYMap, makeEncryptedYText, readAuthMessage, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest };
10700
11072
  //# sourceMappingURL=abracadabra-provider.esm.js.map