@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.
@@ -2822,7 +2822,7 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
2822
2822
  this._client = client;
2823
2823
  this.abracadabraConfig = configuration;
2824
2824
  this.subdocLoading = configuration.subdocLoading ?? "lazy";
2825
- const serverOrigin = AbracadabraProvider.deriveServerOrigin(configuration, client);
2825
+ const serverOrigin = configuration.serverAgnostic ? void 0 : AbracadabraProvider.deriveServerOrigin(configuration, client);
2826
2826
  this.offlineStore = configuration.disableOfflineStore ? null : new OfflineStore(configuration.name, serverOrigin);
2827
2827
  this.on("subdocRegistered", configuration.onSubdocRegistered ?? (() => null));
2828
2828
  this.on("subdocLoaded", configuration.onSubdocLoaded ?? (() => null));
@@ -3252,6 +3252,18 @@ var AbracadabraClient = class {
3252
3252
  auth: false
3253
3253
  });
3254
3254
  }
3255
+ /**
3256
+ * Fetch a short-lived anonymous pairing token for WebRTC signaling.
3257
+ * No authentication required. The token only grants access to `__pairing_*` rooms.
3258
+ */
3259
+ static async getPairingToken(serverUrl) {
3260
+ let base = serverUrl;
3261
+ while (base.endsWith("/")) base = base.slice(0, -1);
3262
+ const resp = await fetch(`${base}/auth/pairing-token`, { method: "POST" });
3263
+ if (!resp.ok) throw new Error(`Failed to fetch pairing token: ${resp.status}`);
3264
+ const { token } = await resp.json();
3265
+ return token;
3266
+ }
3255
3267
  /** Get encryption info for a document. */
3256
3268
  async getDocEncryption(docId) {
3257
3269
  return this.request("GET", `/docs/${encodeURIComponent(docId)}/encryption`);
@@ -8910,8 +8922,8 @@ var SignalingSocket = class extends EventEmitter {
8910
8922
  break;
8911
8923
  case "joined":
8912
8924
  this.emit("joined", {
8913
- peerId: msg.peer_id,
8914
- userId: msg.user_id,
8925
+ peer_id: msg.peer_id,
8926
+ user_id: msg.user_id,
8915
8927
  muted: msg.muted,
8916
8928
  video: msg.video,
8917
8929
  screen: msg.screen,
@@ -10349,6 +10361,7 @@ var ManualSignaling = class extends EventEmitter {
10349
10361
  const CODE_CHARSET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
10350
10362
  const CODE_LENGTH = 6;
10351
10363
  const PAIRING_TIMEOUT_MS = 300 * 1e3;
10364
+ const SIGNALING_CONNECT_TIMEOUT_MS = 5e3;
10352
10365
  function generatePairingCode() {
10353
10366
  const bytes = crypto.getRandomValues(new Uint8Array(CODE_LENGTH));
10354
10367
  return Array.from(bytes).map((b) => CODE_CHARSET[b % 32]).join("");
@@ -10366,6 +10379,7 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
10366
10379
  this._destroyed = false;
10367
10380
  this._pendingRequest = null;
10368
10381
  this._connectedPeerId = null;
10382
+ this._usingFallback = false;
10369
10383
  this.role = role;
10370
10384
  this.pairingCode = pairingCode;
10371
10385
  }
@@ -10520,12 +10534,41 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
10520
10534
  }
10521
10535
  this.removeAllListeners();
10522
10536
  }
10537
+ async resolveToken(serverUrl) {
10538
+ if (this.config.token && serverUrl === this.config.serverUrl) return this.config.token;
10539
+ let base = serverUrl ?? this.config.serverUrl;
10540
+ while (base.endsWith("/")) base = base.slice(0, -1);
10541
+ const resp = await fetch(`${base}/auth/pairing-token`, { method: "POST" });
10542
+ if (!resp.ok) throw new Error(`Failed to fetch pairing token: ${resp.status} ${resp.statusText}`);
10543
+ const { token } = await resp.json();
10544
+ return token;
10545
+ }
10523
10546
  start() {
10547
+ this.connectToServer(this.config.serverUrl);
10548
+ this.timeoutHandle = setTimeout(() => {
10549
+ if (!this._destroyed) {
10550
+ this.emit("error", /* @__PURE__ */ new Error("Pairing timed out"));
10551
+ this.destroy();
10552
+ }
10553
+ }, PAIRING_TIMEOUT_MS);
10554
+ }
10555
+ connectToServer(serverUrl, signalingUrl) {
10556
+ const roomId = codeToRoomId(this.pairingCode);
10557
+ const tokenPromise = this.resolveToken(serverUrl);
10558
+ const tokenFactory = async () => {
10559
+ const t = await tokenPromise;
10560
+ if (typeof t === "function") return await t();
10561
+ return t;
10562
+ };
10563
+ if (!this.config.iceServers) new AbracadabraClient({ url: serverUrl }).getIceServers().then((servers) => {
10564
+ if (servers.length > 0) this._resolvedIceServers = servers;
10565
+ });
10524
10566
  this.webrtc = new AbracadabraWebRTC({
10525
- docId: codeToRoomId(this.pairingCode),
10526
- url: this.config.serverUrl,
10527
- token: this.config.token,
10528
- iceServers: this.config.iceServers,
10567
+ docId: roomId,
10568
+ url: serverUrl,
10569
+ signalingUrl: signalingUrl ?? void 0,
10570
+ token: tokenFactory,
10571
+ iceServers: this.config.iceServers ?? this._resolvedIceServers,
10529
10572
  e2ee: this.config.e2ee,
10530
10573
  enableDocSync: false,
10531
10574
  enableAwarenessSync: false,
@@ -10533,6 +10576,10 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
10533
10576
  autoConnect: false,
10534
10577
  WebSocketPolyfill: this.config.WebSocketPolyfill
10535
10578
  });
10579
+ let connected = false;
10580
+ this.webrtc.on("connected", () => {
10581
+ connected = true;
10582
+ });
10536
10583
  this.webrtc.on("e2eeEstablished", ({ peerId }) => {
10537
10584
  this._connectedPeerId = peerId;
10538
10585
  this.emit("connected");
@@ -10546,14 +10593,25 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
10546
10593
  this.webrtc.on("signalingError", (err) => {
10547
10594
  this.emit("error", /* @__PURE__ */ new Error(`Signaling: ${err.message}`));
10548
10595
  });
10549
- this.timeoutHandle = setTimeout(() => {
10550
- if (!this._destroyed) {
10551
- this.emit("error", /* @__PURE__ */ new Error("Pairing timed out"));
10552
- this.destroy();
10553
- }
10554
- }, PAIRING_TIMEOUT_MS);
10596
+ if (this.config.fallbackSignalingUrl && !signalingUrl) {
10597
+ const fallbackTimer = setTimeout(() => {
10598
+ if (this._destroyed || connected) return;
10599
+ if (this.webrtc) {
10600
+ this.webrtc.destroy();
10601
+ this.webrtc = null;
10602
+ }
10603
+ this._usingFallback = true;
10604
+ this.emit("fallback", { url: this.config.fallbackSignalingUrl });
10605
+ this.connectToServer(this.config.fallbackSignalingUrl);
10606
+ }, SIGNALING_CONNECT_TIMEOUT_MS);
10607
+ this.webrtc.on("connected", () => clearTimeout(fallbackTimer));
10608
+ }
10555
10609
  this.webrtc.connect();
10556
10610
  }
10611
+ /** Whether the connection fell back to the fallback signaling server. */
10612
+ get usingFallback() {
10613
+ return this._usingFallback;
10614
+ }
10557
10615
  sendMessage(msg) {
10558
10616
  if (!this.webrtc || !this._connectedPeerId) return;
10559
10617
  this.webrtc.sendCustomMessage(this._connectedPeerId, JSON.stringify(msg));
@@ -10747,6 +10805,320 @@ var BroadcastChannelSync = class BroadcastChannelSync extends EventEmitter {
10747
10805
  }
10748
10806
  };
10749
10807
 
10808
+ //#endregion
10809
+ //#region packages/provider/src/IdentityDoc.ts
10810
+ /**
10811
+ * Derives a deterministic UUID from an Ed25519 account-level public key.
10812
+ *
10813
+ * The result is a valid UUID v5-style string that any device sharing the
10814
+ * same identity can independently compute. The `abracadabra:identity:`
10815
+ * prefix prevents collisions with randomly generated doc UUIDs.
10816
+ *
10817
+ * @param publicKeyB64 Base64url-encoded Ed25519 public key (32 bytes).
10818
+ */
10819
+ function deriveIdentityDocId(publicKeyB64) {
10820
+ const hash = sha256(new TextEncoder().encode(`abracadabra:identity:${publicKeyB64}`));
10821
+ const bytes = new Uint8Array(hash.buffer, hash.byteOffset, 16);
10822
+ bytes[6] = bytes[6] & 15 | 80;
10823
+ bytes[8] = bytes[8] & 63 | 128;
10824
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
10825
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
10826
+ }
10827
+ /**
10828
+ * Manages a Y.Doc dedicated to user identity that syncs across trusted
10829
+ * targets only: a designated sync server, a local Tauri server, and/or
10830
+ * WebRTC P2P.
10831
+ *
10832
+ * The Y.Doc contains cross-device settings (profile, servers, spaces,
10833
+ * plugins, preferences). Each sync target gets its own
10834
+ * AbracadabraProvider sharing the same Y.Doc, with `serverAgnostic: true`
10835
+ * so they all use one IndexedDB store.
10836
+ */
10837
+ var IdentityDocProvider = class extends EventEmitter {
10838
+ constructor(configuration) {
10839
+ super();
10840
+ this.providers = /* @__PURE__ */ new Map();
10841
+ this.websockets = /* @__PURE__ */ new Map();
10842
+ this.webrtc = null;
10843
+ this._destroyed = false;
10844
+ this.config = configuration;
10845
+ this.docId = deriveIdentityDocId(configuration.publicKey);
10846
+ this.document = new yjs.Doc({ guid: this.docId });
10847
+ if (configuration.autoConnect !== false) this.connect();
10848
+ }
10849
+ connect() {
10850
+ if (this._destroyed) return;
10851
+ const targets = [];
10852
+ if (this.config.localServerUrl) targets.push({
10853
+ url: this.config.localServerUrl,
10854
+ key: "local"
10855
+ });
10856
+ if (this.config.syncServerUrl) targets.push({
10857
+ url: this.config.syncServerUrl,
10858
+ key: "sync"
10859
+ });
10860
+ for (const { url, key } of targets) {
10861
+ if (this.providers.has(key)) continue;
10862
+ this.connectToServer(key, url);
10863
+ }
10864
+ if (this.config.webrtc && !this.webrtc) this._connectWebRTC();
10865
+ }
10866
+ connectToServer(key, serverUrl) {
10867
+ if (this._destroyed) return;
10868
+ const existingProvider = this.providers.get(key);
10869
+ if (existingProvider) {
10870
+ existingProvider.destroy();
10871
+ this.providers.delete(key);
10872
+ }
10873
+ const existingWs = this.websockets.get(key);
10874
+ if (existingWs) {
10875
+ existingWs.destroy();
10876
+ this.websockets.delete(key);
10877
+ }
10878
+ const token = this.config.tokens?.[serverUrl] ?? this.config.token ?? "";
10879
+ const ws = new AbracadabraWS({
10880
+ url: serverUrl.replace(/^http/, "ws").replace(/\/$/, "").concat("/ws"),
10881
+ WebSocketPolyfill: void 0
10882
+ });
10883
+ this.websockets.set(key, ws);
10884
+ const providerConfig = {
10885
+ name: this.docId,
10886
+ document: this.document,
10887
+ websocketProvider: ws,
10888
+ serverAgnostic: true,
10889
+ disableOfflineStore: key !== "local" && key !== "sync" ? true : this.config.disableOfflineStore ?? false,
10890
+ ...this.config.providerDefaults
10891
+ };
10892
+ if (this.config.cryptoIdentity && this.config.signChallenge) {
10893
+ providerConfig.cryptoIdentity = this.config.cryptoIdentity;
10894
+ providerConfig.signChallenge = this.config.signChallenge;
10895
+ } else providerConfig.token = token;
10896
+ const provider = new AbracadabraProvider(providerConfig);
10897
+ provider.on("synced", () => this.emit("synced", { server: key }));
10898
+ provider.on("status", (data) => this.emit("status", {
10899
+ server: key,
10900
+ ...data
10901
+ }));
10902
+ this.providers.set(key, provider);
10903
+ }
10904
+ _connectWebRTC() {
10905
+ const rtcConfig = this.config.webrtc;
10906
+ if (!rtcConfig) return;
10907
+ this.webrtc = new AbracadabraWebRTC({
10908
+ docId: this.docId,
10909
+ url: rtcConfig.signalingServerUrl,
10910
+ token: rtcConfig.token,
10911
+ document: this.document,
10912
+ enableDocSync: true,
10913
+ enableAwarenessSync: false,
10914
+ enableFileTransfer: false,
10915
+ e2ee: rtcConfig.e2ee,
10916
+ iceServers: rtcConfig.iceServers
10917
+ });
10918
+ }
10919
+ get profileMap() {
10920
+ return this.document.getMap("profile");
10921
+ }
10922
+ get serversMap() {
10923
+ return this.document.getMap("servers");
10924
+ }
10925
+ get spacesArray() {
10926
+ return this.document.getArray("spaces");
10927
+ }
10928
+ get pluginsMap() {
10929
+ return this.document.getMap("plugins");
10930
+ }
10931
+ get preferencesMap() {
10932
+ return this.document.getMap("preferences");
10933
+ }
10934
+ getProfile() {
10935
+ const m = this.profileMap;
10936
+ return {
10937
+ username: m.get("username"),
10938
+ displayName: m.get("displayName"),
10939
+ colorName: m.get("colorName"),
10940
+ neutralColorName: m.get("neutralColorName"),
10941
+ locale: m.get("locale"),
10942
+ avatarUrl: m.get("avatarUrl")
10943
+ };
10944
+ }
10945
+ setProfile(profile) {
10946
+ const m = this.profileMap;
10947
+ this.document.transact(() => {
10948
+ for (const [key, value] of Object.entries(profile)) if (value !== void 0) m.set(key, value);
10949
+ });
10950
+ }
10951
+ getServer(url) {
10952
+ const entry = this.serversMap.get(url);
10953
+ if (!entry) return void 0;
10954
+ return {
10955
+ label: entry.get("label") ?? url,
10956
+ hubDocId: entry.get("hubDocId"),
10957
+ entryDocId: entry.get("entryDocId"),
10958
+ defaultRole: entry.get("defaultRole"),
10959
+ spacesEnabled: entry.get("spacesEnabled"),
10960
+ addedAt: entry.get("addedAt") ?? 0
10961
+ };
10962
+ }
10963
+ setServer(url, entry) {
10964
+ const m = this.serversMap;
10965
+ this.document.transact(() => {
10966
+ let yEntry = m.get(url);
10967
+ if (!yEntry) {
10968
+ yEntry = new yjs.Map();
10969
+ m.set(url, yEntry);
10970
+ }
10971
+ for (const [key, value] of Object.entries(entry)) if (value !== void 0) yEntry.set(key, value);
10972
+ });
10973
+ }
10974
+ removeServer(url) {
10975
+ this.serversMap.delete(url);
10976
+ }
10977
+ getServers() {
10978
+ const result = /* @__PURE__ */ new Map();
10979
+ this.serversMap.forEach((entry, url) => {
10980
+ result.set(url, {
10981
+ label: entry.get("label") ?? url,
10982
+ hubDocId: entry.get("hubDocId"),
10983
+ entryDocId: entry.get("entryDocId"),
10984
+ defaultRole: entry.get("defaultRole"),
10985
+ spacesEnabled: entry.get("spacesEnabled"),
10986
+ addedAt: entry.get("addedAt") ?? 0
10987
+ });
10988
+ });
10989
+ return result;
10990
+ }
10991
+ getSpaces() {
10992
+ const result = [];
10993
+ this.spacesArray.forEach((yMap) => {
10994
+ result.push({
10995
+ id: yMap.get("id"),
10996
+ name: yMap.get("name"),
10997
+ type: yMap.get("type") ?? "remote",
10998
+ serverUrl: yMap.get("serverUrl") ?? null,
10999
+ docId: yMap.get("docId"),
11000
+ remoteSpaceId: yMap.get("remoteSpaceId"),
11001
+ visibility: yMap.get("visibility") ?? "private",
11002
+ isHub: yMap.get("isHub") ?? false,
11003
+ order: yMap.get("order") ?? 0,
11004
+ lastSyncedAt: yMap.get("lastSyncedAt"),
11005
+ createdAt: yMap.get("createdAt") ?? 0
11006
+ });
11007
+ });
11008
+ return result;
11009
+ }
11010
+ addSpace(space) {
11011
+ const yMap = new yjs.Map();
11012
+ this.document.transact(() => {
11013
+ for (const [key, value] of Object.entries(space)) if (value !== void 0) yMap.set(key, value);
11014
+ this.spacesArray.push([yMap]);
11015
+ });
11016
+ }
11017
+ removeSpace(spaceId) {
11018
+ const arr = this.spacesArray;
11019
+ for (let i = 0; i < arr.length; i++) if (arr.get(i).get("id") === spaceId) {
11020
+ arr.delete(i, 1);
11021
+ return;
11022
+ }
11023
+ }
11024
+ updateSpace(spaceId, updates) {
11025
+ const arr = this.spacesArray;
11026
+ for (let i = 0; i < arr.length; i++) {
11027
+ const yMap = arr.get(i);
11028
+ if (yMap.get("id") === spaceId) {
11029
+ this.document.transact(() => {
11030
+ for (const [key, value] of Object.entries(updates)) if (value !== void 0) yMap.set(key, value);
11031
+ });
11032
+ return;
11033
+ }
11034
+ }
11035
+ }
11036
+ getExternalPlugins() {
11037
+ let arr = this.pluginsMap.get("external");
11038
+ if (!arr) {
11039
+ arr = new yjs.Array();
11040
+ this.pluginsMap.set("external", arr);
11041
+ }
11042
+ return arr;
11043
+ }
11044
+ getDisabledBuiltins() {
11045
+ let arr = this.pluginsMap.get("disabledBuiltins");
11046
+ if (!arr) {
11047
+ arr = new yjs.Array();
11048
+ this.pluginsMap.set("disabledBuiltins", arr);
11049
+ }
11050
+ return arr;
11051
+ }
11052
+ getPreference(key) {
11053
+ return this.preferencesMap.get(key);
11054
+ }
11055
+ setPreference(key, value) {
11056
+ this.preferencesMap.set(key, value);
11057
+ }
11058
+ /**
11059
+ * Observe deep changes on a specific top-level map.
11060
+ * Returns an unsubscribe function.
11061
+ */
11062
+ observe(mapName, callback) {
11063
+ const map = this.document.getMap(mapName);
11064
+ map.observeDeep(callback);
11065
+ return () => map.unobserveDeep(callback);
11066
+ }
11067
+ /**
11068
+ * Observe changes to the spaces array.
11069
+ * Returns an unsubscribe function.
11070
+ */
11071
+ observeSpaces(callback) {
11072
+ const arr = this.spacesArray;
11073
+ arr.observe(callback);
11074
+ return () => arr.unobserve(callback);
11075
+ }
11076
+ /**
11077
+ * Returns true if the identity doc has no profile data yet (first use).
11078
+ * Call this to decide whether to run migration from localStorage.
11079
+ */
11080
+ isEmpty() {
11081
+ return this.profileMap.size === 0 && this.serversMap.size === 0;
11082
+ }
11083
+ /**
11084
+ * Update the sync server URL at runtime (e.g. when user changes their
11085
+ * designated sync server in settings).
11086
+ */
11087
+ setSyncServer(url) {
11088
+ const existing = this.providers.get("sync");
11089
+ if (existing) {
11090
+ existing.destroy();
11091
+ this.providers.delete("sync");
11092
+ }
11093
+ const existingWs = this.websockets.get("sync");
11094
+ if (existingWs) {
11095
+ existingWs.destroy();
11096
+ this.websockets.delete("sync");
11097
+ }
11098
+ if (url) {
11099
+ this.config = {
11100
+ ...this.config,
11101
+ syncServerUrl: url
11102
+ };
11103
+ this.connectToServer("sync", url);
11104
+ }
11105
+ }
11106
+ getProvider(key) {
11107
+ return this.providers.get(key);
11108
+ }
11109
+ destroy() {
11110
+ if (this._destroyed) return;
11111
+ this._destroyed = true;
11112
+ for (const [, provider] of this.providers) provider.destroy();
11113
+ for (const [, ws] of this.websockets) ws.destroy();
11114
+ if (this.webrtc) this.webrtc.disconnect?.();
11115
+ this.providers.clear();
11116
+ this.websockets.clear();
11117
+ this.webrtc = null;
11118
+ this.document.destroy();
11119
+ }
11120
+ };
11121
+
10750
11122
  //#endregion
10751
11123
  exports.AbracadabraBaseProvider = AbracadabraBaseProvider;
10752
11124
  exports.AbracadabraClient = AbracadabraClient;
@@ -10778,6 +11150,7 @@ exports.FileTransferHandle = FileTransferHandle;
10778
11150
  exports.Forbidden = Forbidden;
10779
11151
  exports.HocuspocusProvider = HocuspocusProvider;
10780
11152
  exports.HocuspocusProviderWebsocket = HocuspocusProviderWebsocket;
11153
+ exports.IdentityDocProvider = IdentityDocProvider;
10781
11154
  exports.KEY_EXCHANGE_CHANNEL = KEY_EXCHANGE_CHANNEL;
10782
11155
  exports.ManualSignaling = ManualSignaling;
10783
11156
  exports.MessageTooBig = MessageTooBig;
@@ -10795,6 +11168,7 @@ exports.YjsDataChannel = YjsDataChannel;
10795
11168
  exports.attachUpdatedAtObserver = attachUpdatedAtObserver;
10796
11169
  exports.awarenessStatesToArray = awarenessStatesToArray;
10797
11170
  exports.decryptField = decryptField;
11171
+ exports.deriveIdentityDocId = deriveIdentityDocId;
10798
11172
  exports.encryptField = encryptField;
10799
11173
  exports.makeEncryptedYMap = makeEncryptedYMap;
10800
11174
  exports.makeEncryptedYText = makeEncryptedYText;