@abraca/dabra 1.0.24 → 1.1.0

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.
@@ -8672,6 +8672,15 @@ var BackgroundSyncManager = class extends EventEmitter {
8672
8672
  const childProvider = await this.rootProvider.loadChild(docId);
8673
8673
  await childProvider.ready;
8674
8674
  await this._waitForSynced(childProvider);
8675
+ {
8676
+ const treeEntry = this.rootProvider.document.getMap("doc-tree").get(docId);
8677
+ this.emit("docSynced", {
8678
+ docId,
8679
+ document: childProvider.document,
8680
+ label: treeEntry?.label ?? "",
8681
+ meta: treeEntry?.meta
8682
+ });
8683
+ }
8675
8684
  if (this.opts.prefetchFiles && this.fileBlobStore) this._prefetchDocFiles(docId, childProvider.document).catch(() => null);
8676
8685
  if (!alreadyCached) this.rootProvider.unloadChild(docId);
8677
8686
  return {
@@ -8701,6 +8710,15 @@ var BackgroundSyncManager = class extends EventEmitter {
8701
8710
  try {
8702
8711
  await childProvider.ready;
8703
8712
  await this._waitForSynced(childProvider);
8713
+ {
8714
+ const treeEntry = this.rootProvider.document.getMap("doc-tree").get(docId);
8715
+ this.emit("docSynced", {
8716
+ docId,
8717
+ document: childDoc,
8718
+ label: treeEntry?.label ?? "",
8719
+ meta: treeEntry?.meta
8720
+ });
8721
+ }
8704
8722
  if (this.opts.prefetchFiles && this.fileBlobStore) this._prefetchDocFiles(docId, childDoc).catch(() => null);
8705
8723
  return {
8706
8724
  docId,
@@ -9987,17 +10005,20 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9987
10005
  const pc = this.peerConnections.get(peerId);
9988
10006
  if (!pc) return;
9989
10007
  const channelName = "custom";
9990
- let channel = pc.router.getChannel(channelName);
9991
- if (!channel || channel.readyState !== "open") {
9992
- channel = pc.router.createChannel(channelName, { ordered: true });
9993
- channel.onopen = () => {
9994
- const data = new TextEncoder().encode(payload);
9995
- pc.router.send(channelName, data).catch(() => {});
9996
- };
10008
+ const channel = pc.router.getChannel(channelName);
10009
+ if (channel && channel.readyState === "open") {
10010
+ const data = new TextEncoder().encode(payload);
10011
+ pc.router.send(channelName, data).catch(() => {});
9997
10012
  return;
9998
10013
  }
9999
- const data = new TextEncoder().encode(payload);
10000
- pc.router.send(channelName, data);
10014
+ const onOpen = ({ name }) => {
10015
+ if (name === channelName) {
10016
+ pc.router.off("channelOpen", onOpen);
10017
+ const data = new TextEncoder().encode(payload);
10018
+ pc.router.send(channelName, data).catch(() => {});
10019
+ }
10020
+ };
10021
+ pc.router.on("channelOpen", onOpen);
10001
10022
  }
10002
10023
  /**
10003
10024
  * Send a custom string message to all connected peers.
@@ -10153,6 +10174,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
10153
10174
  enableAwareness: this.config.enableAwarenessSync,
10154
10175
  enableFileTransfer: this.config.enableFileTransfer
10155
10176
  });
10177
+ pc.router.createChannel("custom", { ordered: true });
10156
10178
  try {
10157
10179
  const sdp = await pc.createOffer();
10158
10180
  this.signaling?.sendOffer(peerId, sdp);
@@ -10949,6 +10971,9 @@ var IdentityDocProvider = class extends EventEmitter {
10949
10971
  get pluginsMap() {
10950
10972
  return this.document.getMap("plugins");
10951
10973
  }
10974
+ get devicesMap() {
10975
+ return this.document.getMap("devices");
10976
+ }
10952
10977
  get preferencesMap() {
10953
10978
  return this.document.getMap("preferences");
10954
10979
  }
@@ -11076,6 +11101,97 @@ var IdentityDocProvider = class extends EventEmitter {
11076
11101
  setPreference(key, value) {
11077
11102
  this.preferencesMap.set(key, value);
11078
11103
  }
11104
+ getDevice(publicKey) {
11105
+ const entry = this.devicesMap.get(publicKey);
11106
+ if (!entry) return void 0;
11107
+ const servers = /* @__PURE__ */ new Map();
11108
+ const serversYMap = entry.get("servers");
11109
+ if (serversYMap) serversYMap.forEach((sEntry, url) => {
11110
+ servers.set(url, {
11111
+ registered: sEntry.get("registered") ?? false,
11112
+ registeredAt: sEntry.get("registeredAt") ?? null,
11113
+ keyId: sEntry.get("keyId") ?? null,
11114
+ lastVerifiedAt: sEntry.get("lastVerifiedAt") ?? null,
11115
+ error: sEntry.get("error") ?? null
11116
+ });
11117
+ });
11118
+ return {
11119
+ deviceName: entry.get("deviceName") ?? "Unknown",
11120
+ tier: entry.get("tier") ?? "paired",
11121
+ x25519Key: entry.get("x25519Key") ?? null,
11122
+ addedAt: entry.get("addedAt") ?? 0,
11123
+ revokedAt: entry.get("revokedAt") ?? null,
11124
+ revokedBy: entry.get("revokedBy") ?? null,
11125
+ servers
11126
+ };
11127
+ }
11128
+ getDevices() {
11129
+ const result = /* @__PURE__ */ new Map();
11130
+ this.devicesMap.forEach((_entry, pubKey) => {
11131
+ const device = this.getDevice(pubKey);
11132
+ if (device) result.set(pubKey, device);
11133
+ });
11134
+ return result;
11135
+ }
11136
+ addDevice(publicKey, opts) {
11137
+ this.document.transact(() => {
11138
+ let entry = this.devicesMap.get(publicKey);
11139
+ if (!entry) {
11140
+ entry = new yjs.Map();
11141
+ this.devicesMap.set(publicKey, entry);
11142
+ }
11143
+ entry.set("deviceName", opts.deviceName);
11144
+ entry.set("tier", opts.tier);
11145
+ if (opts.x25519Key) entry.set("x25519Key", opts.x25519Key);
11146
+ entry.set("addedAt", Date.now());
11147
+ entry.set("revokedAt", null);
11148
+ entry.set("revokedBy", null);
11149
+ if (opts.serverUrl) {
11150
+ let servers = entry.get("servers");
11151
+ if (!servers) {
11152
+ servers = new yjs.Map();
11153
+ entry.set("servers", servers);
11154
+ }
11155
+ const sEntry = new yjs.Map();
11156
+ sEntry.set("registered", true);
11157
+ sEntry.set("registeredAt", Date.now());
11158
+ sEntry.set("keyId", opts.keyId ?? null);
11159
+ sEntry.set("lastVerifiedAt", Date.now());
11160
+ sEntry.set("error", null);
11161
+ servers.set(opts.serverUrl, sEntry);
11162
+ }
11163
+ });
11164
+ }
11165
+ revokeDevice(publicKey, revokedBy) {
11166
+ const entry = this.devicesMap.get(publicKey);
11167
+ if (!entry) return;
11168
+ this.document.transact(() => {
11169
+ entry.set("revokedAt", Date.now());
11170
+ entry.set("revokedBy", revokedBy);
11171
+ });
11172
+ }
11173
+ setDeviceServerStatus(devicePubKey, serverUrl, status) {
11174
+ const entry = this.devicesMap.get(devicePubKey);
11175
+ if (!entry) return;
11176
+ this.document.transact(() => {
11177
+ let servers = entry.get("servers");
11178
+ if (!servers) {
11179
+ servers = new yjs.Map();
11180
+ entry.set("servers", servers);
11181
+ }
11182
+ let sEntry = servers.get(serverUrl);
11183
+ if (!sEntry) {
11184
+ sEntry = new yjs.Map();
11185
+ sEntry.set("registered", false);
11186
+ sEntry.set("registeredAt", null);
11187
+ sEntry.set("keyId", null);
11188
+ sEntry.set("lastVerifiedAt", null);
11189
+ sEntry.set("error", null);
11190
+ servers.set(serverUrl, sEntry);
11191
+ }
11192
+ for (const [key, value] of Object.entries(status)) if (value !== void 0) sEntry.set(key, value);
11193
+ });
11194
+ }
11079
11195
  /**
11080
11196
  * Observe deep changes on a specific top-level map.
11081
11197
  * Returns an unsubscribe function.
@@ -11140,6 +11256,154 @@ var IdentityDocProvider = class extends EventEmitter {
11140
11256
  }
11141
11257
  };
11142
11258
 
11259
+ //#endregion
11260
+ //#region packages/provider/src/DeviceRegistrationService.ts
11261
+ /**
11262
+ * Handles cross-server device key registration and revocation propagation.
11263
+ *
11264
+ * All methods operate on the identity Y.Doc's `devicesMap` and authenticate
11265
+ * independently on each server using the master key's `signChallenge`.
11266
+ */
11267
+ var DeviceRegistrationService = class {
11268
+ /**
11269
+ * Fan out a device key to all servers in `serversMap` where it isn't registered.
11270
+ *
11271
+ * Must be called from a master device that can sign challenges with the
11272
+ * account-level private key.
11273
+ */
11274
+ static async fanOutDeviceKey(opts) {
11275
+ const { devicePubKey, x25519Key, deviceName, identityDoc, masterPubKey, signChallenge, skipServerUrl } = opts;
11276
+ const servers = identityDoc.getServers();
11277
+ const results = [];
11278
+ if (!identityDoc.getDevice(devicePubKey)) identityDoc.addDevice(devicePubKey, {
11279
+ deviceName: deviceName ?? "Unknown",
11280
+ tier: "paired",
11281
+ x25519Key
11282
+ });
11283
+ for (const [serverUrl] of servers) {
11284
+ if (skipServerUrl && serverUrl === skipServerUrl) continue;
11285
+ if ((identityDoc.getDevice(devicePubKey)?.servers.get(serverUrl))?.registered) continue;
11286
+ try {
11287
+ const client = new AbracadabraClient({ url: serverUrl });
11288
+ await client.loginWithKey(masterPubKey, signChallenge);
11289
+ await client.addKey({
11290
+ publicKey: devicePubKey,
11291
+ x25519Key,
11292
+ deviceName
11293
+ });
11294
+ const match = (await client.listKeys()).find((k) => k.publicKey === devicePubKey && !k.revoked);
11295
+ identityDoc.setDeviceServerStatus(devicePubKey, serverUrl, {
11296
+ registered: true,
11297
+ registeredAt: Date.now(),
11298
+ keyId: match?.id ?? null,
11299
+ lastVerifiedAt: Date.now(),
11300
+ error: null
11301
+ });
11302
+ results.push({
11303
+ serverUrl,
11304
+ success: true
11305
+ });
11306
+ } catch (e) {
11307
+ identityDoc.setDeviceServerStatus(devicePubKey, serverUrl, { error: e?.message ?? "Registration failed" });
11308
+ results.push({
11309
+ serverUrl,
11310
+ success: false,
11311
+ error: e?.message ?? "Registration failed"
11312
+ });
11313
+ }
11314
+ }
11315
+ return results;
11316
+ }
11317
+ /**
11318
+ * Propagate pending revocations from the identity Y.Doc to all servers.
11319
+ *
11320
+ * Scans `devicesMap` for entries with `revokedAt` set and revokes them
11321
+ * on every server where they're still registered. Must be called from
11322
+ * a master device.
11323
+ */
11324
+ static async propagateRevocations(opts) {
11325
+ const { identityDoc, masterPubKey, signChallenge } = opts;
11326
+ const devices = identityDoc.getDevices();
11327
+ const results = [];
11328
+ for (const [devicePubKey, device] of devices) {
11329
+ if (!device.revokedAt) continue;
11330
+ for (const [serverUrl, serverStatus] of device.servers) {
11331
+ if (!serverStatus.registered || !serverStatus.keyId) continue;
11332
+ try {
11333
+ const client = new AbracadabraClient({ url: serverUrl });
11334
+ await client.loginWithKey(masterPubKey, signChallenge);
11335
+ await client.revokeKey(serverStatus.keyId);
11336
+ identityDoc.setDeviceServerStatus(devicePubKey, serverUrl, {
11337
+ registered: false,
11338
+ error: null
11339
+ });
11340
+ results.push({
11341
+ serverUrl,
11342
+ success: true
11343
+ });
11344
+ } catch (e) {
11345
+ results.push({
11346
+ serverUrl,
11347
+ success: false,
11348
+ error: e?.message ?? "Revocation failed"
11349
+ });
11350
+ }
11351
+ }
11352
+ }
11353
+ return results;
11354
+ }
11355
+ /**
11356
+ * Reconcile the identity Y.Doc's `devicesMap` with a server's actual
11357
+ * device key list. Call this on every server connection.
11358
+ *
11359
+ * - Updates registration status in Y.Doc to match server reality
11360
+ * - Imports unknown server keys as legacy entries
11361
+ * - Executes pending revocations if the caller is a master device
11362
+ */
11363
+ static async reconcile(opts) {
11364
+ const { identityDoc, serverUrl, client, isMaster } = opts;
11365
+ let serverKeys;
11366
+ try {
11367
+ serverKeys = await client.listKeys();
11368
+ } catch {
11369
+ return;
11370
+ }
11371
+ const devicesMap = identityDoc.devicesMap;
11372
+ devicesMap.forEach((yEntry, devicePub) => {
11373
+ const serverEntry = yEntry.get("servers")?.get(serverUrl);
11374
+ const serverKey = serverKeys.find((k) => k.publicKey === devicePub);
11375
+ const revokedAt = yEntry.get("revokedAt");
11376
+ if (serverEntry?.get("registered") && !serverKey) identityDoc.setDeviceServerStatus(devicePub, serverUrl, {
11377
+ registered: false,
11378
+ keyId: null
11379
+ });
11380
+ if (serverKey && !serverKey.revoked) {
11381
+ if (!serverEntry?.get("registered")) identityDoc.setDeviceServerStatus(devicePub, serverUrl, {
11382
+ registered: true,
11383
+ keyId: serverKey.id,
11384
+ lastVerifiedAt: Date.now()
11385
+ });
11386
+ else identityDoc.setDeviceServerStatus(devicePub, serverUrl, {
11387
+ lastVerifiedAt: Date.now(),
11388
+ keyId: serverKey.id
11389
+ });
11390
+ if (revokedAt && isMaster) client.revokeKey(serverKey.id).then(() => {
11391
+ identityDoc.setDeviceServerStatus(devicePub, serverUrl, { registered: false });
11392
+ }).catch(() => {});
11393
+ }
11394
+ });
11395
+ for (const key of serverKeys) {
11396
+ if (key.revoked) continue;
11397
+ if (!devicesMap.has(key.publicKey)) identityDoc.addDevice(key.publicKey, {
11398
+ deviceName: key.deviceName ?? "Unknown",
11399
+ tier: "paired",
11400
+ serverUrl,
11401
+ keyId: key.id
11402
+ });
11403
+ }
11404
+ }
11405
+ };
11406
+
11143
11407
  //#endregion
11144
11408
  exports.AbracadabraBaseProvider = AbracadabraBaseProvider;
11145
11409
  exports.AbracadabraClient = AbracadabraClient;
@@ -11158,6 +11422,7 @@ exports.DEFAULT_FILE_CHUNK_SIZE = DEFAULT_FILE_CHUNK_SIZE;
11158
11422
  exports.DEFAULT_ICE_SERVERS = DEFAULT_ICE_SERVERS;
11159
11423
  exports.DataChannelRouter = DataChannelRouter;
11160
11424
  exports.DevicePairingChannel = DevicePairingChannel;
11425
+ exports.DeviceRegistrationService = DeviceRegistrationService;
11161
11426
  exports.DocKeyManager = DocKeyManager;
11162
11427
  exports.DocumentCache = DocumentCache;
11163
11428
  exports.E2EAbracadabraProvider = E2EAbracadabraProvider;