@abraca/dabra 1.6.0 → 1.8.1

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.
@@ -2810,6 +2810,7 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
2810
2810
  this.childAccessTimes = /* @__PURE__ */ new Map();
2811
2811
  this.pinnedChildren = /* @__PURE__ */ new Set();
2812
2812
  this.boundHandleYSubdocsChange = this.handleYSubdocsChange.bind(this);
2813
+ this.hasCachedContent = false;
2813
2814
  this._client = client;
2814
2815
  this.abracadabraConfig = configuration;
2815
2816
  this.subdocLoading = configuration.subdocLoading ?? "lazy";
@@ -2847,8 +2848,14 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
2847
2848
  async _initFromOfflineStore() {
2848
2849
  if (!this.offlineStore) return;
2849
2850
  const [snapshot, pending] = await Promise.all([this.offlineStore.getDocSnapshot().catch(() => null), this.offlineStore.getPendingUpdates().catch(() => [])]);
2850
- if (snapshot) Y.applyUpdate(this.document, snapshot, this.offlineStore);
2851
- for (const update of pending) Y.applyUpdate(this.document, update, this.offlineStore);
2851
+ if (snapshot) {
2852
+ Y.applyUpdate(this.document, snapshot, this.offlineStore);
2853
+ this.hasCachedContent = true;
2854
+ }
2855
+ for (const update of pending) {
2856
+ Y.applyUpdate(this.document, update, this.offlineStore);
2857
+ this.hasCachedContent = true;
2858
+ }
2852
2859
  }
2853
2860
  authenticatedHandler(scope) {
2854
2861
  super.authenticatedHandler(scope);
@@ -13152,8 +13159,12 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
13152
13159
  /**
13153
13160
  * Approve the pending pairing request. Calls `client.addKey()` to
13154
13161
  * register Device B's public key, then notifies Device B.
13162
+ *
13163
+ * @param client Authenticated REST client.
13164
+ * @param masterPublicKey Approver's account-level Ed25519 public key (base64url).
13165
+ * Sent to Device B so it can adopt the master's identity doc.
13155
13166
  */
13156
- async approve(client) {
13167
+ async approve(client, masterPublicKey) {
13157
13168
  if (this.role !== "approver") return {
13158
13169
  success: false,
13159
13170
  error: "Only the approver can approve"
@@ -13173,7 +13184,10 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
13173
13184
  deviceName: req.deviceName,
13174
13185
  x25519Key: req.x25519Key
13175
13186
  });
13176
- this.sendMessage({ type: "pair-approved" });
13187
+ this.sendMessage({
13188
+ type: "pair-approved",
13189
+ masterPublicKey
13190
+ });
13177
13191
  this._pendingRequest = null;
13178
13192
  this.emit("pairingComplete", { success: true });
13179
13193
  return { success: true };
@@ -13196,8 +13210,12 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
13196
13210
  * Approve via server-side device invite. Creates a single-use invite code
13197
13211
  * and sends it to Device B over the E2EE channel. Device B redeems it
13198
13212
  * independently via HTTP — Device A can go offline after this.
13213
+ *
13214
+ * @param client Authenticated REST client.
13215
+ * @param masterPublicKey Approver's account-level Ed25519 public key (base64url).
13216
+ * Sent to Device B so it can adopt the master's identity doc.
13199
13217
  */
13200
- async approveWithInvite(client) {
13218
+ async approveWithInvite(client, masterPublicKey) {
13201
13219
  if (this.role !== "approver") return {
13202
13220
  success: false,
13203
13221
  error: "Only the approver can approve"
@@ -13214,7 +13232,8 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
13214
13232
  const { code } = await client.createDeviceInvite();
13215
13233
  this.sendMessage({
13216
13234
  type: "pair-invite-code",
13217
- code
13235
+ code,
13236
+ masterPublicKey
13218
13237
  });
13219
13238
  this._pendingRequest = null;
13220
13239
  this.emit("pairingComplete", { success: true });
@@ -13385,7 +13404,7 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
13385
13404
  break;
13386
13405
  case "pair-approved":
13387
13406
  if (this.role !== "requester") return;
13388
- this.emit("approved");
13407
+ this.emit("approved", { masterPublicKey: msg.masterPublicKey });
13389
13408
  this.emit("pairingComplete", { success: true });
13390
13409
  break;
13391
13410
  case "pair-rejected":
@@ -13398,7 +13417,7 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
13398
13417
  break;
13399
13418
  case "pair-invite-code":
13400
13419
  if (this.role !== "requester") return;
13401
- this.emit("inviteCode", msg.code);
13420
+ this.emit("inviteCode", msg.code, msg.masterPublicKey);
13402
13421
  break;
13403
13422
  }
13404
13423
  }
@@ -13925,6 +13944,19 @@ var IdentityDocProvider = class extends EventEmitter {
13925
13944
  return this.profileMap.size === 0 && this.serversMap.size === 0;
13926
13945
  }
13927
13946
  /**
13947
+ * Enable WebRTC P2P sync at runtime.
13948
+ * Use this for claimed/passkey users where E2EE identity derivation
13949
+ * was deferred to avoid biometric prompts on page load.
13950
+ */
13951
+ enableWebRTC(webrtcConfig) {
13952
+ if (this._destroyed || this.webrtc) return;
13953
+ this.config = {
13954
+ ...this.config,
13955
+ webrtc: webrtcConfig
13956
+ };
13957
+ this._connectWebRTC();
13958
+ }
13959
+ /**
13928
13960
  * Update the sync server URL at runtime (e.g. when user changes their
13929
13961
  * designated sync server in settings).
13930
13962
  */
@@ -14112,5 +14144,318 @@ var DeviceRegistrationService = class {
14112
14144
  };
14113
14145
 
14114
14146
  //#endregion
14115
- export { AbracadabraBaseProvider, AbracadabraClient, AbracadabraProvider, AbracadabraWS, AbracadabraWebRTC, AuthMessageType, AwarenessError, BackgroundSyncManager, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ConnectionTimeout, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, DevicePairingChannel, DeviceRegistrationService, 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, wordlist as bip39Wordlist, decryptField, deriveIdentityDocId, deriveSeedWrappingKey, encryptField, generateMnemonic, makeEncryptedYMap, makeEncryptedYText, mnemonicToEd25519Seed, mnemonicToKeyPair, readAuthMessage, unwrapSeed, validateMnemonic, wrapSeed, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest };
14147
+ //#region packages/provider/src/ChatClient.ts
14148
+ const DEFAULT_TIMEOUT_MS$1 = 1e4;
14149
+ /**
14150
+ * Typed client for the Abracadabra chat feature.
14151
+ *
14152
+ * Wraps a connected provider (or base provider) and translates JSON envelopes
14153
+ * on the stateless channel into typed method calls and events.
14154
+ *
14155
+ * Events emitted:
14156
+ * - `message` → ChatMessage (new message broadcast)
14157
+ * - `typing` → ChatTypingEvent (typing indicator broadcast)
14158
+ * - `readReceipt` → ChatReadReceipt (mark_read broadcast)
14159
+ */
14160
+ var ChatClient = class extends EventEmitter {
14161
+ constructor(provider, options) {
14162
+ super();
14163
+ this.pending = /* @__PURE__ */ new Map();
14164
+ this.provider = provider;
14165
+ this.responseTimeoutMs = options?.responseTimeoutMs ?? DEFAULT_TIMEOUT_MS$1;
14166
+ this.boundOnStateless = (data) => this.handleStateless(data.payload);
14167
+ this.provider.on("stateless", this.boundOnStateless);
14168
+ }
14169
+ /** Stop listening for chat messages. Does not disconnect the underlying provider. */
14170
+ destroy() {
14171
+ this.provider.off("stateless", this.boundOnStateless);
14172
+ for (const queue of this.pending.values()) for (const p of queue) {
14173
+ clearTimeout(p.timer);
14174
+ p.reject(/* @__PURE__ */ new Error("ChatClient destroyed"));
14175
+ }
14176
+ this.pending.clear();
14177
+ this.removeAllListeners();
14178
+ }
14179
+ /** Send a chat message on a channel. Fire-and-forget (the server broadcasts). */
14180
+ sendMessage(input) {
14181
+ this.provider.sendStateless(JSON.stringify({
14182
+ type: "chat:send",
14183
+ channel: input.channel,
14184
+ content: input.content,
14185
+ ...input.sender_name !== void 0 ? { sender_name: input.sender_name } : {}
14186
+ }));
14187
+ }
14188
+ /** Fetch historical messages for a channel. Resolves with the server response. */
14189
+ getHistory(input) {
14190
+ const promise = this.enqueue("chat:history");
14191
+ this.provider.sendStateless(JSON.stringify({
14192
+ type: "chat:history",
14193
+ channel: input.channel,
14194
+ ...input.before !== void 0 ? { before: input.before } : {},
14195
+ ...input.limit !== void 0 ? { limit: input.limit } : {}
14196
+ }));
14197
+ return promise;
14198
+ }
14199
+ /** Broadcast a typing indicator on a channel. */
14200
+ sendTyping(channel) {
14201
+ this.provider.sendStateless(JSON.stringify({
14202
+ type: "chat:typing",
14203
+ channel
14204
+ }));
14205
+ }
14206
+ /** List the current user's channels (ordered by last activity). */
14207
+ listChannels() {
14208
+ const promise = this.enqueue("chat:channels");
14209
+ this.provider.sendStateless(JSON.stringify({ type: "chat:channels" }));
14210
+ return promise;
14211
+ }
14212
+ /** Mark a channel read up to `timestamp` (unix ms). */
14213
+ markRead(channel, timestamp) {
14214
+ this.provider.sendStateless(JSON.stringify({
14215
+ type: "chat:mark_read",
14216
+ channel,
14217
+ timestamp
14218
+ }));
14219
+ }
14220
+ /** Fetch per-user read cursors for a channel. */
14221
+ getReadCursors(channel) {
14222
+ const promise = this.enqueue("chat:read_cursors");
14223
+ this.provider.sendStateless(JSON.stringify({
14224
+ type: "chat:read_cursors",
14225
+ channel
14226
+ }));
14227
+ return promise;
14228
+ }
14229
+ onMessage(fn) {
14230
+ return this.on("message", fn);
14231
+ }
14232
+ onTyping(fn) {
14233
+ return this.on("typing", fn);
14234
+ }
14235
+ onReadReceipt(fn) {
14236
+ return this.on("readReceipt", fn);
14237
+ }
14238
+ enqueue(type) {
14239
+ return new Promise((resolve, reject) => {
14240
+ const entry = {
14241
+ resolve,
14242
+ reject,
14243
+ timer: setTimeout(() => {
14244
+ this.removePending(type, entry);
14245
+ reject(/* @__PURE__ */ new Error(`ChatClient: timeout waiting for ${type} response`));
14246
+ }, this.responseTimeoutMs)
14247
+ };
14248
+ const queue = this.pending.get(type) ?? [];
14249
+ queue.push(entry);
14250
+ this.pending.set(type, queue);
14251
+ });
14252
+ }
14253
+ removePending(type, entry) {
14254
+ const queue = this.pending.get(type);
14255
+ if (!queue) return;
14256
+ const idx = queue.indexOf(entry);
14257
+ if (idx >= 0) queue.splice(idx, 1);
14258
+ if (queue.length === 0) this.pending.delete(type);
14259
+ }
14260
+ resolveNext(type, value) {
14261
+ const queue = this.pending.get(type);
14262
+ if (!queue || queue.length === 0) return false;
14263
+ const next = queue.shift();
14264
+ if (queue.length === 0) this.pending.delete(type);
14265
+ clearTimeout(next.timer);
14266
+ next.resolve(value);
14267
+ return true;
14268
+ }
14269
+ handleStateless(payload) {
14270
+ let parsed;
14271
+ try {
14272
+ parsed = JSON.parse(payload);
14273
+ } catch {
14274
+ return;
14275
+ }
14276
+ const type = parsed?.type;
14277
+ if (typeof type !== "string" || !type.startsWith("chat:")) return;
14278
+ switch (type) {
14279
+ case "chat:message": {
14280
+ const { type: _t, ...rest } = parsed;
14281
+ this.emit("message", rest);
14282
+ break;
14283
+ }
14284
+ case "chat:history":
14285
+ this.resolveNext("chat:history", {
14286
+ channel: parsed.channel,
14287
+ messages: parsed.messages ?? []
14288
+ });
14289
+ break;
14290
+ case "chat:channels":
14291
+ this.resolveNext("chat:channels", { channels: parsed.channels ?? [] });
14292
+ break;
14293
+ case "chat:typing":
14294
+ this.emit("typing", {
14295
+ channel: parsed.channel,
14296
+ sender_id: parsed.sender_id,
14297
+ sender_name: parsed.sender_name ?? null
14298
+ });
14299
+ break;
14300
+ case "chat:read_receipt":
14301
+ this.emit("readReceipt", {
14302
+ channel: parsed.channel,
14303
+ user_id: parsed.user_id,
14304
+ last_read_at: parsed.last_read_at
14305
+ });
14306
+ break;
14307
+ case "chat:read_cursors":
14308
+ this.resolveNext("chat:read_cursors", {
14309
+ channel: parsed.channel,
14310
+ cursors: parsed.cursors ?? []
14311
+ });
14312
+ break;
14313
+ default: break;
14314
+ }
14315
+ }
14316
+ };
14317
+
14318
+ //#endregion
14319
+ //#region packages/provider/src/NotificationsClient.ts
14320
+ const DEFAULT_TIMEOUT_MS = 1e4;
14321
+ /**
14322
+ * Typed client for the Abracadabra notifications feature.
14323
+ *
14324
+ * Emits:
14325
+ * - `new` → NotificationRecord (incoming notify:new broadcast)
14326
+ * - `readUpdate` → NotificationReadUpdate (broadcast after mark_read / mark_all_read)
14327
+ */
14328
+ var NotificationsClient = class extends EventEmitter {
14329
+ constructor(provider, options) {
14330
+ super();
14331
+ this.pending = /* @__PURE__ */ new Map();
14332
+ this.provider = provider;
14333
+ this.responseTimeoutMs = options?.responseTimeoutMs ?? DEFAULT_TIMEOUT_MS;
14334
+ this.boundOnStateless = (data) => this.handleStateless(data.payload);
14335
+ this.provider.on("stateless", this.boundOnStateless);
14336
+ }
14337
+ destroy() {
14338
+ this.provider.off("stateless", this.boundOnStateless);
14339
+ for (const queue of this.pending.values()) for (const p of queue) {
14340
+ clearTimeout(p.timer);
14341
+ p.reject(/* @__PURE__ */ new Error("NotificationsClient destroyed"));
14342
+ }
14343
+ this.pending.clear();
14344
+ this.removeAllListeners();
14345
+ }
14346
+ /**
14347
+ * Create a notification targeting a specific recipient. Requires elevated role
14348
+ * (service or admin); a `server:error` event with code `forbidden` is emitted
14349
+ * by the underlying provider if the caller lacks permission.
14350
+ */
14351
+ create(input) {
14352
+ this.provider.sendStateless(JSON.stringify({
14353
+ type: "notify:create",
14354
+ recipient_id: input.recipient_id,
14355
+ ...input.notification_type !== void 0 ? { notification_type: input.notification_type } : {},
14356
+ title: input.title,
14357
+ ...input.body !== void 0 ? { body: input.body } : {},
14358
+ ...input.icon !== void 0 ? { icon: input.icon } : {},
14359
+ ...input.link !== void 0 ? { link: input.link } : {},
14360
+ ...input.source_id !== void 0 ? { source_id: input.source_id } : {}
14361
+ }));
14362
+ }
14363
+ /** Fetch notification history for the current user. */
14364
+ fetch(input = {}) {
14365
+ const promise = this.enqueue("notify:history");
14366
+ this.provider.sendStateless(JSON.stringify({
14367
+ type: "notify:fetch",
14368
+ ...input.before !== void 0 ? { before: input.before } : {},
14369
+ ...input.limit !== void 0 ? { limit: input.limit } : {},
14370
+ ...input.unread_only !== void 0 ? { unread_only: input.unread_only } : {}
14371
+ }));
14372
+ return promise;
14373
+ }
14374
+ /** Mark a single notification, or a batch, as read. */
14375
+ markRead(target) {
14376
+ const body = { type: "notify:mark_read" };
14377
+ if ("id" in target) body.id = target.id;
14378
+ else body.ids = target.ids;
14379
+ this.provider.sendStateless(JSON.stringify(body));
14380
+ }
14381
+ /** Mark every notification for the current user as read. */
14382
+ markAllRead() {
14383
+ this.provider.sendStateless(JSON.stringify({ type: "notify:mark_all_read" }));
14384
+ }
14385
+ /** Mark all notifications generated by the given source (e.g. a chat channel) as read. */
14386
+ markReadBySource(sourceId) {
14387
+ this.provider.sendStateless(JSON.stringify({
14388
+ type: "notify:mark_read_by_source",
14389
+ source_id: sourceId
14390
+ }));
14391
+ }
14392
+ onNew(fn) {
14393
+ return this.on("new", fn);
14394
+ }
14395
+ onReadUpdate(fn) {
14396
+ return this.on("readUpdate", fn);
14397
+ }
14398
+ enqueue(type) {
14399
+ return new Promise((resolve, reject) => {
14400
+ const entry = {
14401
+ resolve,
14402
+ reject,
14403
+ timer: setTimeout(() => {
14404
+ this.removePending(type, entry);
14405
+ reject(/* @__PURE__ */ new Error(`NotificationsClient: timeout waiting for ${type} response`));
14406
+ }, this.responseTimeoutMs)
14407
+ };
14408
+ const queue = this.pending.get(type) ?? [];
14409
+ queue.push(entry);
14410
+ this.pending.set(type, queue);
14411
+ });
14412
+ }
14413
+ removePending(type, entry) {
14414
+ const queue = this.pending.get(type);
14415
+ if (!queue) return;
14416
+ const idx = queue.indexOf(entry);
14417
+ if (idx >= 0) queue.splice(idx, 1);
14418
+ if (queue.length === 0) this.pending.delete(type);
14419
+ }
14420
+ resolveNext(type, value) {
14421
+ const queue = this.pending.get(type);
14422
+ if (!queue || queue.length === 0) return false;
14423
+ const next = queue.shift();
14424
+ if (queue.length === 0) this.pending.delete(type);
14425
+ clearTimeout(next.timer);
14426
+ next.resolve(value);
14427
+ return true;
14428
+ }
14429
+ handleStateless(payload) {
14430
+ let parsed;
14431
+ try {
14432
+ parsed = JSON.parse(payload);
14433
+ } catch {
14434
+ return;
14435
+ }
14436
+ const type = parsed?.type;
14437
+ if (typeof type !== "string" || !type.startsWith("notify:")) return;
14438
+ switch (type) {
14439
+ case "notify:new": {
14440
+ const { type: _t, ...rest } = parsed;
14441
+ this.emit("new", rest);
14442
+ break;
14443
+ }
14444
+ case "notify:history":
14445
+ this.resolveNext("notify:history", { notifications: parsed.notifications ?? [] });
14446
+ break;
14447
+ case "notify:read_update": {
14448
+ const update = { recipient_id: parsed.recipient_id };
14449
+ if (parsed.ids !== void 0) update.ids = parsed.ids;
14450
+ if (parsed.all !== void 0) update.all = parsed.all;
14451
+ this.emit("readUpdate", update);
14452
+ break;
14453
+ }
14454
+ default: break;
14455
+ }
14456
+ }
14457
+ };
14458
+
14459
+ //#endregion
14460
+ export { AbracadabraBaseProvider, AbracadabraClient, AbracadabraProvider, AbracadabraWS, AbracadabraWebRTC, AuthMessageType, AwarenessError, BackgroundSyncManager, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ChatClient, ConnectionTimeout, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, DevicePairingChannel, DeviceRegistrationService, DocKeyManager, DocumentCache, E2EAbracadabraProvider, E2EEChannel, E2EOfflineStore, EncryptedYMap, EncryptedYText, FileBlobStore, FileTransferChannel, FileTransferHandle, Forbidden, HocuspocusProvider, HocuspocusProviderWebsocket, IdentityDocProvider, KEY_EXCHANGE_CHANNEL, ManualSignaling, MessageTooBig, MessageType, NotificationsClient, OfflineStore, PeerConnection, ResetConnection, SearchIndex, SignalingSocket, SubdocMessage, Unauthorized, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, wordlist as bip39Wordlist, decryptField, deriveIdentityDocId, deriveSeedWrappingKey, encryptField, generateMnemonic, makeEncryptedYMap, makeEncryptedYText, mnemonicToEd25519Seed, mnemonicToKeyPair, readAuthMessage, unwrapSeed, validateMnemonic, wrapSeed, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest };
14116
14461
  //# sourceMappingURL=abracadabra-provider.esm.js.map