@agentvault/agentvault 0.19.34 → 0.19.35

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.
package/dist/cli.js CHANGED
@@ -44744,7 +44744,7 @@ var init_ratchet = __esm({
44744
44744
  async "../crypto/dist/ratchet.js"() {
44745
44745
  "use strict";
44746
44746
  await init_libsodium_wrappers();
44747
- MAX_SKIP = 100;
44747
+ MAX_SKIP = 1e3;
44748
44748
  CHAIN_KEY_SEED = new Uint8Array([1]);
44749
44749
  MSG_KEY_SEED = new Uint8Array([2]);
44750
44750
  HEADER_KEY_SEED = new Uint8Array([3]);
@@ -47175,6 +47175,12 @@ var init_channel = __esm({
47175
47175
  this._primaryConversationId = this._persisted.primaryConversationId;
47176
47176
  this._fingerprint = this._persisted.fingerprint;
47177
47177
  this._lastInboundRoomId = this._persisted.lastInboundRoomId;
47178
+ if (this._persisted.seenMessageIds) {
47179
+ this._seenMessageIds = new Set(this._persisted.seenMessageIds.slice(-_SecureChannel.SEEN_MSG_MAX));
47180
+ }
47181
+ if (this._persisted.seenA2AMessageIds) {
47182
+ this._a2aSeenMessageIds = new Set(this._persisted.seenA2AMessageIds.slice(-_SecureChannel.A2A_SEEN_MAX));
47183
+ }
47178
47184
  for (const [convId, sessionData] of Object.entries(
47179
47185
  this._persisted.sessions
47180
47186
  )) {
@@ -47321,6 +47327,7 @@ var init_channel = __esm({
47321
47327
  }
47322
47328
  const messageGroupId = randomUUID2();
47323
47329
  let sentCount = 0;
47330
+ const pendingWsSends = [];
47324
47331
  for (const [convId, session] of this._sessions) {
47325
47332
  if (!session.activated) continue;
47326
47333
  if (roomConvIds.has(convId)) continue;
@@ -47353,7 +47360,7 @@ var init_channel = __esm({
47353
47360
  if (this._persisted?.hubId) {
47354
47361
  payload.sender_hub_id = this._persisted.hubId;
47355
47362
  }
47356
- this._ws.send(
47363
+ pendingWsSends.push(
47357
47364
  JSON.stringify({
47358
47365
  event: "message",
47359
47366
  data: payload
@@ -47379,6 +47386,9 @@ var init_channel = __esm({
47379
47386
  console.warn("[SecureChannel] send() delivered to 0 sessions (all skipped or failed)");
47380
47387
  }
47381
47388
  await this._persistState();
47389
+ for (const frame of pendingWsSends) {
47390
+ this._ws.send(frame);
47391
+ }
47382
47392
  }
47383
47393
  /**
47384
47394
  * Send a typing indicator to all owner devices.
@@ -47657,6 +47667,7 @@ var init_channel = __esm({
47657
47667
  if (recipients.length === 0) {
47658
47668
  throw new Error("No active sessions in room");
47659
47669
  }
47670
+ await this._persistState();
47660
47671
  if (this._state === "ready" && this._ws) {
47661
47672
  this._ws.send(
47662
47673
  JSON.stringify({
@@ -47696,7 +47707,6 @@ var init_channel = __esm({
47696
47707
  throw new Error(`Failed to send room message: ${err}`);
47697
47708
  }
47698
47709
  }
47699
- await this._persistState();
47700
47710
  }
47701
47711
  /**
47702
47712
  * Leave a room: remove sessions and persisted room state.
@@ -47795,11 +47805,12 @@ var init_channel = __esm({
47795
47805
  attachment: attachMeta
47796
47806
  });
47797
47807
  const messageGroupId = randomUUID2();
47808
+ const pendingWsSends = [];
47798
47809
  for (const [convId, session] of this._sessions) {
47799
47810
  if (!session.activated) continue;
47800
47811
  const encrypted = session.ratchet.encrypt(envelope);
47801
47812
  const transport = encryptedMessageToTransport(encrypted);
47802
- this._ws.send(
47813
+ pendingWsSends.push(
47803
47814
  JSON.stringify({
47804
47815
  event: "message",
47805
47816
  data: {
@@ -47813,6 +47824,9 @@ var init_channel = __esm({
47813
47824
  );
47814
47825
  }
47815
47826
  await this._persistState();
47827
+ for (const frame of pendingWsSends) {
47828
+ this._ws.send(frame);
47829
+ }
47816
47830
  }
47817
47831
  async sendActionConfirmation(confirmation) {
47818
47832
  const envelope = {
@@ -49629,11 +49643,12 @@ ${messageText}`;
49629
49643
  });
49630
49644
  this._appendHistory("agent", plaintext, topicId);
49631
49645
  const messageGroupId = randomUUID2();
49646
+ const pendingWsSends = [];
49632
49647
  for (const [convId, session] of this._sessions) {
49633
49648
  if (!session.activated) continue;
49634
49649
  const encrypted = session.ratchet.encrypt(envelope);
49635
49650
  const transport = encryptedMessageToTransport(encrypted);
49636
- this._ws.send(
49651
+ pendingWsSends.push(
49637
49652
  JSON.stringify({
49638
49653
  event: "message",
49639
49654
  data: {
@@ -49647,6 +49662,9 @@ ${messageText}`;
49647
49662
  );
49648
49663
  }
49649
49664
  await this._persistState();
49665
+ for (const frame of pendingWsSends) {
49666
+ this._ws.send(frame);
49667
+ }
49650
49668
  }
49651
49669
  /**
49652
49670
  * Relay an owner's message to all sibling sessions as encrypted sync messages.
@@ -49669,13 +49687,14 @@ ${messageText}`;
49669
49687
  ts: (/* @__PURE__ */ new Date()).toISOString(),
49670
49688
  topicId
49671
49689
  });
49690
+ const pendingWsSends = [];
49672
49691
  for (const [siblingConvId, siblingSession] of this._sessions) {
49673
49692
  if (siblingConvId === sourceConvId) continue;
49674
49693
  if (!siblingSession.activated) continue;
49675
49694
  if (roomConvIds.has(siblingConvId)) continue;
49676
49695
  const syncEncrypted = siblingSession.ratchet.encrypt(syncPayload);
49677
49696
  const syncTransport = encryptedMessageToTransport(syncEncrypted);
49678
- this._ws.send(
49697
+ pendingWsSends.push(
49679
49698
  JSON.stringify({
49680
49699
  event: "message",
49681
49700
  data: {
@@ -49686,6 +49705,10 @@ ${messageText}`;
49686
49705
  })
49687
49706
  );
49688
49707
  }
49708
+ await this._persistState();
49709
+ for (const frame of pendingWsSends) {
49710
+ this._ws.send(frame);
49711
+ }
49689
49712
  }
49690
49713
  /**
49691
49714
  * Resolve the agent's workspace directory.
@@ -49729,6 +49752,7 @@ ${messageText}`;
49729
49752
  const plaintext = JSON.stringify(payload);
49730
49753
  const encrypted = session.ratchet.encrypt(plaintext);
49731
49754
  const transport = encryptedMessageToTransport(encrypted);
49755
+ await this._persistState();
49732
49756
  this._ws.send(
49733
49757
  JSON.stringify({
49734
49758
  event: "message",
@@ -49739,7 +49763,6 @@ ${messageText}`;
49739
49763
  }
49740
49764
  })
49741
49765
  );
49742
- await this._persistState();
49743
49766
  }
49744
49767
  /**
49745
49768
  * Send stored message history to a newly-activated session.
@@ -49762,6 +49785,7 @@ ${messageText}`;
49762
49785
  });
49763
49786
  const encrypted = session.ratchet.encrypt(replayPayload);
49764
49787
  const transport = encryptedMessageToTransport(encrypted);
49788
+ await this._persistState();
49765
49789
  this._ws.send(
49766
49790
  JSON.stringify({
49767
49791
  event: "message",
@@ -50014,15 +50038,31 @@ ${messageText}`;
50014
50038
  console.log(
50015
50039
  `[SecureChannel] Room ratchet re-initialized for conv ${convId.slice(0, 8)}...`
50016
50040
  );
50017
- plaintext = session.ratchet.decrypt(encrypted);
50018
- session.activated = true;
50019
- if (this._persisted.sessions[convId]) {
50020
- this._persisted.sessions[convId].activated = true;
50041
+ const incomingMsgNum = encrypted.header.messageNumber;
50042
+ if (incomingMsgNum <= 5) {
50043
+ try {
50044
+ plaintext = session.ratchet.decrypt(encrypted);
50045
+ session.activated = true;
50046
+ if (this._persisted.sessions[convId]) {
50047
+ this._persisted.sessions[convId].activated = true;
50048
+ }
50049
+ await this._persistState();
50050
+ console.log(
50051
+ `[SecureChannel] Room session ${convId.slice(0, 8)}... re-activated after ratchet re-init`
50052
+ );
50053
+ } catch (retryErr) {
50054
+ console.warn(
50055
+ `[SecureChannel] Room re-init retry failed for conv ${convId.slice(0, 8)} (msgNum=${incomingMsgNum}):`,
50056
+ retryErr
50057
+ );
50058
+ return;
50059
+ }
50060
+ } else {
50061
+ console.log(
50062
+ `[SecureChannel] Room re-init: skipping message with msgNum=${incomingMsgNum} for conv ${convId.slice(0, 8)} (too far ahead for fresh ratchet)`
50063
+ );
50064
+ return;
50021
50065
  }
50022
- await this._persistState();
50023
- console.log(
50024
- `[SecureChannel] Room session ${convId.slice(0, 8)}... re-activated after ratchet re-init`
50025
- );
50026
50066
  } catch (reinitErr) {
50027
50067
  console.error(
50028
50068
  `[SecureChannel] Room ratchet re-init failed for conv ${convId.slice(0, 8)}...:`,
@@ -50201,13 +50241,14 @@ ${messageText}`;
50201
50241
  const dist = chain.getDistribution(this._deviceId);
50202
50242
  const distJson = JSON.stringify(dist);
50203
50243
  const alreadyDistributed = new Set(room.distributedTo ?? []);
50244
+ const pendingWsSends = [];
50204
50245
  for (const convId of room.conversationIds) {
50205
50246
  const session = this._sessions.get(convId);
50206
50247
  if (!session || alreadyDistributed.has(session.ownerDeviceId)) continue;
50207
50248
  const encrypted = session.ratchet.encrypt(distJson);
50208
50249
  const transport = encryptedMessageToTransport(encrypted);
50209
50250
  if (this._state === "ready" && this._ws) {
50210
- this._ws.send(
50251
+ pendingWsSends.push(
50211
50252
  JSON.stringify({
50212
50253
  event: "sender_key_distribution",
50213
50254
  data: {
@@ -50223,6 +50264,9 @@ ${messageText}`;
50223
50264
  }
50224
50265
  room.distributedTo = [...alreadyDistributed];
50225
50266
  await this._persistState();
50267
+ for (const frame of pendingWsSends) {
50268
+ this._ws.send(frame);
50269
+ }
50226
50270
  }
50227
50271
  /**
50228
50272
  * Handle an incoming sender_key_distribution event.
@@ -50420,8 +50464,10 @@ ${messageText}`;
50420
50464
  try {
50421
50465
  a2aPlaintext = ratchet.decrypt(encryptedMessage);
50422
50466
  } catch (decryptErr) {
50423
- console.error(`[SecureChannel] A2A decrypt failed \u2014 restoring ratchet state:`, decryptErr);
50467
+ console.error(`[SecureChannel] A2A decrypt failed \u2014 restoring ratchet, initiating rekey:`, decryptErr);
50424
50468
  channelEntry.session.ratchetState = ratchetSnapshot;
50469
+ await this._persistState();
50470
+ await this._initiateA2ARekey(channelId);
50425
50471
  return;
50426
50472
  }
50427
50473
  channelEntry.session.ratchetState = ratchet.serialize();
@@ -50463,24 +50509,9 @@ ${messageText}`;
50463
50509
  }
50464
50510
  } else {
50465
50511
  console.warn(
50466
- `[SecureChannel] Received encrypted A2A but no session for ${channelId.slice(0, 8)} \u2014 triggering key exchange`
50512
+ `[SecureChannel] Received encrypted A2A but no session for ${channelId.slice(0, 8)} \u2014 initiating rekey`
50467
50513
  );
50468
- const entry = this._persisted?.a2aChannels?.[channelId];
50469
- if (entry && !entry.pendingEphemeralPrivateKey && !entry.session) {
50470
- try {
50471
- const a2aEphemeral = await generateEphemeralKeypair();
50472
- const ephPubHex = bytesToHex(a2aEphemeral.publicKey);
50473
- entry.pendingEphemeralPrivateKey = bytesToHex(a2aEphemeral.privateKey);
50474
- this._ws?.send(JSON.stringify({
50475
- event: "a2a_key_exchange",
50476
- data: { channel_id: channelId, ephemeral_key: ephPubHex }
50477
- }));
50478
- await this._persistState();
50479
- console.log(`[SecureChannel] On-demand A2A key exchange for ${channelId.slice(0, 8)}`);
50480
- } catch (kxErr) {
50481
- console.warn(`[SecureChannel] On-demand key exchange failed:`, kxErr);
50482
- }
50483
- }
50514
+ await this._initiateA2ARekey(channelId);
50484
50515
  }
50485
50516
  } else {
50486
50517
  const a2aMsg = {
@@ -50497,6 +50528,51 @@ ${messageText}`;
50497
50528
  }
50498
50529
  }
50499
50530
  }
50531
+ /**
50532
+ * Initiate A2A channel rekey after decrypt failure.
50533
+ * Calls the backend to reset channel to approved, clears local session,
50534
+ * and submits a fresh ephemeral key for key exchange.
50535
+ */
50536
+ async _initiateA2ARekey(channelId) {
50537
+ const entry = this._persisted?.a2aChannels?.[channelId];
50538
+ if (!entry || !this._deviceJwt || !this._ws) return;
50539
+ const now = Date.now();
50540
+ const lastRekey = entry._lastRekeyAttempt ?? 0;
50541
+ if (now - lastRekey < 3e4) return;
50542
+ entry._lastRekeyAttempt = now;
50543
+ console.log(`[SecureChannel] Initiating A2A rekey for ${channelId.slice(0, 8)}...`);
50544
+ try {
50545
+ const res = await fetch(
50546
+ `${this.config.apiUrl}/api/v1/a2a/channels/${channelId}/rekey`,
50547
+ {
50548
+ method: "POST",
50549
+ headers: { Authorization: `Bearer ${this._deviceJwt}` }
50550
+ }
50551
+ );
50552
+ if (!res.ok) {
50553
+ const detail = await res.text();
50554
+ console.warn(`[SecureChannel] A2A rekey API failed (${res.status}): ${detail}`);
50555
+ return;
50556
+ }
50557
+ delete entry.session;
50558
+ delete entry.observerSession;
50559
+ delete entry.pendingEphemeralPrivateKey;
50560
+ delete entry.pendingObserverEphemeralPrivateKey;
50561
+ const a2aEphemeral = await generateEphemeralKeypair();
50562
+ entry.pendingEphemeralPrivateKey = bytesToHex(a2aEphemeral.privateKey);
50563
+ this._ws.send(JSON.stringify({
50564
+ event: "a2a_key_exchange",
50565
+ data: {
50566
+ channel_id: channelId,
50567
+ ephemeral_key: bytesToHex(a2aEphemeral.publicKey)
50568
+ }
50569
+ }));
50570
+ await this._persistState();
50571
+ console.log(`[SecureChannel] A2A rekey submitted for ${channelId.slice(0, 8)}`);
50572
+ } catch (err) {
50573
+ console.warn(`[SecureChannel] A2A rekey failed:`, err);
50574
+ }
50575
+ }
50500
50576
  /**
50501
50577
  * Paginated sync: fetch missed messages in pages of 200, up to 5 pages (1000 messages).
50502
50578
  * Tracks message IDs in _syncMessageIds to prevent duplicate processing from concurrent WS messages.
@@ -50917,6 +50993,8 @@ ${messageText}`;
50917
50993
  const hasA2AChannels = !!this._persisted.a2aChannels && Object.keys(this._persisted.a2aChannels).length > 0;
50918
50994
  if (!hasOwnerSessions && !hasA2AChannels) return;
50919
50995
  this._persisted.lastInboundRoomId = this._lastInboundRoomId;
50996
+ this._persisted.seenMessageIds = [...this._seenMessageIds].slice(-_SecureChannel.SEEN_MSG_MAX);
50997
+ this._persisted.seenA2AMessageIds = [...this._a2aSeenMessageIds].slice(-_SecureChannel.A2A_SEEN_MAX);
50920
50998
  for (const [convId, session] of this._sessions) {
50921
50999
  this._persisted.sessions[convId] = {
50922
51000
  ownerDeviceId: session.ownerDeviceId,