@abraca/dabra 1.6.0 → 1.8.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.
@@ -13191,8 +13191,12 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
13191
13191
  /**
13192
13192
  * Approve the pending pairing request. Calls `client.addKey()` to
13193
13193
  * register Device B's public key, then notifies Device B.
13194
+ *
13195
+ * @param client Authenticated REST client.
13196
+ * @param masterPublicKey Approver's account-level Ed25519 public key (base64url).
13197
+ * Sent to Device B so it can adopt the master's identity doc.
13194
13198
  */
13195
- async approve(client) {
13199
+ async approve(client, masterPublicKey) {
13196
13200
  if (this.role !== "approver") return {
13197
13201
  success: false,
13198
13202
  error: "Only the approver can approve"
@@ -13212,7 +13216,10 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
13212
13216
  deviceName: req.deviceName,
13213
13217
  x25519Key: req.x25519Key
13214
13218
  });
13215
- this.sendMessage({ type: "pair-approved" });
13219
+ this.sendMessage({
13220
+ type: "pair-approved",
13221
+ masterPublicKey
13222
+ });
13216
13223
  this._pendingRequest = null;
13217
13224
  this.emit("pairingComplete", { success: true });
13218
13225
  return { success: true };
@@ -13235,8 +13242,12 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
13235
13242
  * Approve via server-side device invite. Creates a single-use invite code
13236
13243
  * and sends it to Device B over the E2EE channel. Device B redeems it
13237
13244
  * independently via HTTP — Device A can go offline after this.
13245
+ *
13246
+ * @param client Authenticated REST client.
13247
+ * @param masterPublicKey Approver's account-level Ed25519 public key (base64url).
13248
+ * Sent to Device B so it can adopt the master's identity doc.
13238
13249
  */
13239
- async approveWithInvite(client) {
13250
+ async approveWithInvite(client, masterPublicKey) {
13240
13251
  if (this.role !== "approver") return {
13241
13252
  success: false,
13242
13253
  error: "Only the approver can approve"
@@ -13253,7 +13264,8 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
13253
13264
  const { code } = await client.createDeviceInvite();
13254
13265
  this.sendMessage({
13255
13266
  type: "pair-invite-code",
13256
- code
13267
+ code,
13268
+ masterPublicKey
13257
13269
  });
13258
13270
  this._pendingRequest = null;
13259
13271
  this.emit("pairingComplete", { success: true });
@@ -13424,7 +13436,7 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
13424
13436
  break;
13425
13437
  case "pair-approved":
13426
13438
  if (this.role !== "requester") return;
13427
- this.emit("approved");
13439
+ this.emit("approved", { masterPublicKey: msg.masterPublicKey });
13428
13440
  this.emit("pairingComplete", { success: true });
13429
13441
  break;
13430
13442
  case "pair-rejected":
@@ -13437,7 +13449,7 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
13437
13449
  break;
13438
13450
  case "pair-invite-code":
13439
13451
  if (this.role !== "requester") return;
13440
- this.emit("inviteCode", msg.code);
13452
+ this.emit("inviteCode", msg.code, msg.masterPublicKey);
13441
13453
  break;
13442
13454
  }
13443
13455
  }
@@ -13964,6 +13976,19 @@ var IdentityDocProvider = class extends EventEmitter {
13964
13976
  return this.profileMap.size === 0 && this.serversMap.size === 0;
13965
13977
  }
13966
13978
  /**
13979
+ * Enable WebRTC P2P sync at runtime.
13980
+ * Use this for claimed/passkey users where E2EE identity derivation
13981
+ * was deferred to avoid biometric prompts on page load.
13982
+ */
13983
+ enableWebRTC(webrtcConfig) {
13984
+ if (this._destroyed || this.webrtc) return;
13985
+ this.config = {
13986
+ ...this.config,
13987
+ webrtc: webrtcConfig
13988
+ };
13989
+ this._connectWebRTC();
13990
+ }
13991
+ /**
13967
13992
  * Update the sync server URL at runtime (e.g. when user changes their
13968
13993
  * designated sync server in settings).
13969
13994
  */
@@ -14150,6 +14175,319 @@ var DeviceRegistrationService = class {
14150
14175
  }
14151
14176
  };
14152
14177
 
14178
+ //#endregion
14179
+ //#region packages/provider/src/ChatClient.ts
14180
+ const DEFAULT_TIMEOUT_MS$1 = 1e4;
14181
+ /**
14182
+ * Typed client for the Abracadabra chat feature.
14183
+ *
14184
+ * Wraps a connected provider (or base provider) and translates JSON envelopes
14185
+ * on the stateless channel into typed method calls and events.
14186
+ *
14187
+ * Events emitted:
14188
+ * - `message` → ChatMessage (new message broadcast)
14189
+ * - `typing` → ChatTypingEvent (typing indicator broadcast)
14190
+ * - `readReceipt` → ChatReadReceipt (mark_read broadcast)
14191
+ */
14192
+ var ChatClient = class extends EventEmitter {
14193
+ constructor(provider, options) {
14194
+ super();
14195
+ this.pending = /* @__PURE__ */ new Map();
14196
+ this.provider = provider;
14197
+ this.responseTimeoutMs = options?.responseTimeoutMs ?? DEFAULT_TIMEOUT_MS$1;
14198
+ this.boundOnStateless = (data) => this.handleStateless(data.payload);
14199
+ this.provider.on("stateless", this.boundOnStateless);
14200
+ }
14201
+ /** Stop listening for chat messages. Does not disconnect the underlying provider. */
14202
+ destroy() {
14203
+ this.provider.off("stateless", this.boundOnStateless);
14204
+ for (const queue of this.pending.values()) for (const p of queue) {
14205
+ clearTimeout(p.timer);
14206
+ p.reject(/* @__PURE__ */ new Error("ChatClient destroyed"));
14207
+ }
14208
+ this.pending.clear();
14209
+ this.removeAllListeners();
14210
+ }
14211
+ /** Send a chat message on a channel. Fire-and-forget (the server broadcasts). */
14212
+ sendMessage(input) {
14213
+ this.provider.sendStateless(JSON.stringify({
14214
+ type: "chat:send",
14215
+ channel: input.channel,
14216
+ content: input.content,
14217
+ ...input.sender_name !== void 0 ? { sender_name: input.sender_name } : {}
14218
+ }));
14219
+ }
14220
+ /** Fetch historical messages for a channel. Resolves with the server response. */
14221
+ getHistory(input) {
14222
+ const promise = this.enqueue("chat:history");
14223
+ this.provider.sendStateless(JSON.stringify({
14224
+ type: "chat:history",
14225
+ channel: input.channel,
14226
+ ...input.before !== void 0 ? { before: input.before } : {},
14227
+ ...input.limit !== void 0 ? { limit: input.limit } : {}
14228
+ }));
14229
+ return promise;
14230
+ }
14231
+ /** Broadcast a typing indicator on a channel. */
14232
+ sendTyping(channel) {
14233
+ this.provider.sendStateless(JSON.stringify({
14234
+ type: "chat:typing",
14235
+ channel
14236
+ }));
14237
+ }
14238
+ /** List the current user's channels (ordered by last activity). */
14239
+ listChannels() {
14240
+ const promise = this.enqueue("chat:channels");
14241
+ this.provider.sendStateless(JSON.stringify({ type: "chat:channels" }));
14242
+ return promise;
14243
+ }
14244
+ /** Mark a channel read up to `timestamp` (unix ms). */
14245
+ markRead(channel, timestamp) {
14246
+ this.provider.sendStateless(JSON.stringify({
14247
+ type: "chat:mark_read",
14248
+ channel,
14249
+ timestamp
14250
+ }));
14251
+ }
14252
+ /** Fetch per-user read cursors for a channel. */
14253
+ getReadCursors(channel) {
14254
+ const promise = this.enqueue("chat:read_cursors");
14255
+ this.provider.sendStateless(JSON.stringify({
14256
+ type: "chat:read_cursors",
14257
+ channel
14258
+ }));
14259
+ return promise;
14260
+ }
14261
+ onMessage(fn) {
14262
+ return this.on("message", fn);
14263
+ }
14264
+ onTyping(fn) {
14265
+ return this.on("typing", fn);
14266
+ }
14267
+ onReadReceipt(fn) {
14268
+ return this.on("readReceipt", fn);
14269
+ }
14270
+ enqueue(type) {
14271
+ return new Promise((resolve, reject) => {
14272
+ const entry = {
14273
+ resolve,
14274
+ reject,
14275
+ timer: setTimeout(() => {
14276
+ this.removePending(type, entry);
14277
+ reject(/* @__PURE__ */ new Error(`ChatClient: timeout waiting for ${type} response`));
14278
+ }, this.responseTimeoutMs)
14279
+ };
14280
+ const queue = this.pending.get(type) ?? [];
14281
+ queue.push(entry);
14282
+ this.pending.set(type, queue);
14283
+ });
14284
+ }
14285
+ removePending(type, entry) {
14286
+ const queue = this.pending.get(type);
14287
+ if (!queue) return;
14288
+ const idx = queue.indexOf(entry);
14289
+ if (idx >= 0) queue.splice(idx, 1);
14290
+ if (queue.length === 0) this.pending.delete(type);
14291
+ }
14292
+ resolveNext(type, value) {
14293
+ const queue = this.pending.get(type);
14294
+ if (!queue || queue.length === 0) return false;
14295
+ const next = queue.shift();
14296
+ if (queue.length === 0) this.pending.delete(type);
14297
+ clearTimeout(next.timer);
14298
+ next.resolve(value);
14299
+ return true;
14300
+ }
14301
+ handleStateless(payload) {
14302
+ let parsed;
14303
+ try {
14304
+ parsed = JSON.parse(payload);
14305
+ } catch {
14306
+ return;
14307
+ }
14308
+ const type = parsed?.type;
14309
+ if (typeof type !== "string" || !type.startsWith("chat:")) return;
14310
+ switch (type) {
14311
+ case "chat:message": {
14312
+ const { type: _t, ...rest } = parsed;
14313
+ this.emit("message", rest);
14314
+ break;
14315
+ }
14316
+ case "chat:history":
14317
+ this.resolveNext("chat:history", {
14318
+ channel: parsed.channel,
14319
+ messages: parsed.messages ?? []
14320
+ });
14321
+ break;
14322
+ case "chat:channels":
14323
+ this.resolveNext("chat:channels", { channels: parsed.channels ?? [] });
14324
+ break;
14325
+ case "chat:typing":
14326
+ this.emit("typing", {
14327
+ channel: parsed.channel,
14328
+ sender_id: parsed.sender_id,
14329
+ sender_name: parsed.sender_name ?? null
14330
+ });
14331
+ break;
14332
+ case "chat:read_receipt":
14333
+ this.emit("readReceipt", {
14334
+ channel: parsed.channel,
14335
+ user_id: parsed.user_id,
14336
+ last_read_at: parsed.last_read_at
14337
+ });
14338
+ break;
14339
+ case "chat:read_cursors":
14340
+ this.resolveNext("chat:read_cursors", {
14341
+ channel: parsed.channel,
14342
+ cursors: parsed.cursors ?? []
14343
+ });
14344
+ break;
14345
+ default: break;
14346
+ }
14347
+ }
14348
+ };
14349
+
14350
+ //#endregion
14351
+ //#region packages/provider/src/NotificationsClient.ts
14352
+ const DEFAULT_TIMEOUT_MS = 1e4;
14353
+ /**
14354
+ * Typed client for the Abracadabra notifications feature.
14355
+ *
14356
+ * Emits:
14357
+ * - `new` → NotificationRecord (incoming notify:new broadcast)
14358
+ * - `readUpdate` → NotificationReadUpdate (broadcast after mark_read / mark_all_read)
14359
+ */
14360
+ var NotificationsClient = class extends EventEmitter {
14361
+ constructor(provider, options) {
14362
+ super();
14363
+ this.pending = /* @__PURE__ */ new Map();
14364
+ this.provider = provider;
14365
+ this.responseTimeoutMs = options?.responseTimeoutMs ?? DEFAULT_TIMEOUT_MS;
14366
+ this.boundOnStateless = (data) => this.handleStateless(data.payload);
14367
+ this.provider.on("stateless", this.boundOnStateless);
14368
+ }
14369
+ destroy() {
14370
+ this.provider.off("stateless", this.boundOnStateless);
14371
+ for (const queue of this.pending.values()) for (const p of queue) {
14372
+ clearTimeout(p.timer);
14373
+ p.reject(/* @__PURE__ */ new Error("NotificationsClient destroyed"));
14374
+ }
14375
+ this.pending.clear();
14376
+ this.removeAllListeners();
14377
+ }
14378
+ /**
14379
+ * Create a notification targeting a specific recipient. Requires elevated role
14380
+ * (service or admin); a `server:error` event with code `forbidden` is emitted
14381
+ * by the underlying provider if the caller lacks permission.
14382
+ */
14383
+ create(input) {
14384
+ this.provider.sendStateless(JSON.stringify({
14385
+ type: "notify:create",
14386
+ recipient_id: input.recipient_id,
14387
+ ...input.notification_type !== void 0 ? { notification_type: input.notification_type } : {},
14388
+ title: input.title,
14389
+ ...input.body !== void 0 ? { body: input.body } : {},
14390
+ ...input.icon !== void 0 ? { icon: input.icon } : {},
14391
+ ...input.link !== void 0 ? { link: input.link } : {},
14392
+ ...input.source_id !== void 0 ? { source_id: input.source_id } : {}
14393
+ }));
14394
+ }
14395
+ /** Fetch notification history for the current user. */
14396
+ fetch(input = {}) {
14397
+ const promise = this.enqueue("notify:history");
14398
+ this.provider.sendStateless(JSON.stringify({
14399
+ type: "notify:fetch",
14400
+ ...input.before !== void 0 ? { before: input.before } : {},
14401
+ ...input.limit !== void 0 ? { limit: input.limit } : {},
14402
+ ...input.unread_only !== void 0 ? { unread_only: input.unread_only } : {}
14403
+ }));
14404
+ return promise;
14405
+ }
14406
+ /** Mark a single notification, or a batch, as read. */
14407
+ markRead(target) {
14408
+ const body = { type: "notify:mark_read" };
14409
+ if ("id" in target) body.id = target.id;
14410
+ else body.ids = target.ids;
14411
+ this.provider.sendStateless(JSON.stringify(body));
14412
+ }
14413
+ /** Mark every notification for the current user as read. */
14414
+ markAllRead() {
14415
+ this.provider.sendStateless(JSON.stringify({ type: "notify:mark_all_read" }));
14416
+ }
14417
+ /** Mark all notifications generated by the given source (e.g. a chat channel) as read. */
14418
+ markReadBySource(sourceId) {
14419
+ this.provider.sendStateless(JSON.stringify({
14420
+ type: "notify:mark_read_by_source",
14421
+ source_id: sourceId
14422
+ }));
14423
+ }
14424
+ onNew(fn) {
14425
+ return this.on("new", fn);
14426
+ }
14427
+ onReadUpdate(fn) {
14428
+ return this.on("readUpdate", fn);
14429
+ }
14430
+ enqueue(type) {
14431
+ return new Promise((resolve, reject) => {
14432
+ const entry = {
14433
+ resolve,
14434
+ reject,
14435
+ timer: setTimeout(() => {
14436
+ this.removePending(type, entry);
14437
+ reject(/* @__PURE__ */ new Error(`NotificationsClient: timeout waiting for ${type} response`));
14438
+ }, this.responseTimeoutMs)
14439
+ };
14440
+ const queue = this.pending.get(type) ?? [];
14441
+ queue.push(entry);
14442
+ this.pending.set(type, queue);
14443
+ });
14444
+ }
14445
+ removePending(type, entry) {
14446
+ const queue = this.pending.get(type);
14447
+ if (!queue) return;
14448
+ const idx = queue.indexOf(entry);
14449
+ if (idx >= 0) queue.splice(idx, 1);
14450
+ if (queue.length === 0) this.pending.delete(type);
14451
+ }
14452
+ resolveNext(type, value) {
14453
+ const queue = this.pending.get(type);
14454
+ if (!queue || queue.length === 0) return false;
14455
+ const next = queue.shift();
14456
+ if (queue.length === 0) this.pending.delete(type);
14457
+ clearTimeout(next.timer);
14458
+ next.resolve(value);
14459
+ return true;
14460
+ }
14461
+ handleStateless(payload) {
14462
+ let parsed;
14463
+ try {
14464
+ parsed = JSON.parse(payload);
14465
+ } catch {
14466
+ return;
14467
+ }
14468
+ const type = parsed?.type;
14469
+ if (typeof type !== "string" || !type.startsWith("notify:")) return;
14470
+ switch (type) {
14471
+ case "notify:new": {
14472
+ const { type: _t, ...rest } = parsed;
14473
+ this.emit("new", rest);
14474
+ break;
14475
+ }
14476
+ case "notify:history":
14477
+ this.resolveNext("notify:history", { notifications: parsed.notifications ?? [] });
14478
+ break;
14479
+ case "notify:read_update": {
14480
+ const update = { recipient_id: parsed.recipient_id };
14481
+ if (parsed.ids !== void 0) update.ids = parsed.ids;
14482
+ if (parsed.all !== void 0) update.all = parsed.all;
14483
+ this.emit("readUpdate", update);
14484
+ break;
14485
+ }
14486
+ default: break;
14487
+ }
14488
+ }
14489
+ };
14490
+
14153
14491
  //#endregion
14154
14492
  exports.AbracadabraBaseProvider = AbracadabraBaseProvider;
14155
14493
  exports.AbracadabraClient = AbracadabraClient;
@@ -14162,6 +14500,7 @@ exports.BackgroundSyncManager = BackgroundSyncManager;
14162
14500
  exports.BackgroundSyncPersistence = BackgroundSyncPersistence;
14163
14501
  exports.BroadcastChannelSync = BroadcastChannelSync;
14164
14502
  exports.CHANNEL_NAMES = CHANNEL_NAMES;
14503
+ exports.ChatClient = ChatClient;
14165
14504
  exports.ConnectionTimeout = ConnectionTimeout;
14166
14505
  exports.CryptoIdentityKeystore = CryptoIdentityKeystore;
14167
14506
  exports.DEFAULT_FILE_CHUNK_SIZE = DEFAULT_FILE_CHUNK_SIZE;
@@ -14187,6 +14526,7 @@ exports.KEY_EXCHANGE_CHANNEL = KEY_EXCHANGE_CHANNEL;
14187
14526
  exports.ManualSignaling = ManualSignaling;
14188
14527
  exports.MessageTooBig = MessageTooBig;
14189
14528
  exports.MessageType = MessageType;
14529
+ exports.NotificationsClient = NotificationsClient;
14190
14530
  exports.OfflineStore = OfflineStore;
14191
14531
  exports.PeerConnection = PeerConnection;
14192
14532
  exports.ResetConnection = ResetConnection;