@agentvault/agentvault 0.19.33 → 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 = {
@@ -48824,6 +48838,9 @@ var init_channel = __esm({
48824
48838
  this._lastWakeTick = Date.now();
48825
48839
  try {
48826
48840
  const data = JSON.parse(raw.toString());
48841
+ if (data.event && data.event !== "ping" && data.event !== "typing") {
48842
+ console.log(`[SecureChannel] WS event: ${data.event} conv=${(data.conversation_id || data.data?.conversation_id || "").toString().slice(0, 8)}`);
48843
+ }
48827
48844
  if (data.event === "ping") {
48828
48845
  ws.send(JSON.stringify({ event: "pong" }));
48829
48846
  return;
@@ -49626,11 +49643,12 @@ ${messageText}`;
49626
49643
  });
49627
49644
  this._appendHistory("agent", plaintext, topicId);
49628
49645
  const messageGroupId = randomUUID2();
49646
+ const pendingWsSends = [];
49629
49647
  for (const [convId, session] of this._sessions) {
49630
49648
  if (!session.activated) continue;
49631
49649
  const encrypted = session.ratchet.encrypt(envelope);
49632
49650
  const transport = encryptedMessageToTransport(encrypted);
49633
- this._ws.send(
49651
+ pendingWsSends.push(
49634
49652
  JSON.stringify({
49635
49653
  event: "message",
49636
49654
  data: {
@@ -49644,6 +49662,9 @@ ${messageText}`;
49644
49662
  );
49645
49663
  }
49646
49664
  await this._persistState();
49665
+ for (const frame of pendingWsSends) {
49666
+ this._ws.send(frame);
49667
+ }
49647
49668
  }
49648
49669
  /**
49649
49670
  * Relay an owner's message to all sibling sessions as encrypted sync messages.
@@ -49666,13 +49687,14 @@ ${messageText}`;
49666
49687
  ts: (/* @__PURE__ */ new Date()).toISOString(),
49667
49688
  topicId
49668
49689
  });
49690
+ const pendingWsSends = [];
49669
49691
  for (const [siblingConvId, siblingSession] of this._sessions) {
49670
49692
  if (siblingConvId === sourceConvId) continue;
49671
49693
  if (!siblingSession.activated) continue;
49672
49694
  if (roomConvIds.has(siblingConvId)) continue;
49673
49695
  const syncEncrypted = siblingSession.ratchet.encrypt(syncPayload);
49674
49696
  const syncTransport = encryptedMessageToTransport(syncEncrypted);
49675
- this._ws.send(
49697
+ pendingWsSends.push(
49676
49698
  JSON.stringify({
49677
49699
  event: "message",
49678
49700
  data: {
@@ -49683,6 +49705,10 @@ ${messageText}`;
49683
49705
  })
49684
49706
  );
49685
49707
  }
49708
+ await this._persistState();
49709
+ for (const frame of pendingWsSends) {
49710
+ this._ws.send(frame);
49711
+ }
49686
49712
  }
49687
49713
  /**
49688
49714
  * Resolve the agent's workspace directory.
@@ -49726,6 +49752,7 @@ ${messageText}`;
49726
49752
  const plaintext = JSON.stringify(payload);
49727
49753
  const encrypted = session.ratchet.encrypt(plaintext);
49728
49754
  const transport = encryptedMessageToTransport(encrypted);
49755
+ await this._persistState();
49729
49756
  this._ws.send(
49730
49757
  JSON.stringify({
49731
49758
  event: "message",
@@ -49736,7 +49763,6 @@ ${messageText}`;
49736
49763
  }
49737
49764
  })
49738
49765
  );
49739
- await this._persistState();
49740
49766
  }
49741
49767
  /**
49742
49768
  * Send stored message history to a newly-activated session.
@@ -49759,6 +49785,7 @@ ${messageText}`;
49759
49785
  });
49760
49786
  const encrypted = session.ratchet.encrypt(replayPayload);
49761
49787
  const transport = encryptedMessageToTransport(encrypted);
49788
+ await this._persistState();
49762
49789
  this._ws.send(
49763
49790
  JSON.stringify({
49764
49791
  event: "message",
@@ -50011,15 +50038,31 @@ ${messageText}`;
50011
50038
  console.log(
50012
50039
  `[SecureChannel] Room ratchet re-initialized for conv ${convId.slice(0, 8)}...`
50013
50040
  );
50014
- plaintext = session.ratchet.decrypt(encrypted);
50015
- session.activated = true;
50016
- if (this._persisted.sessions[convId]) {
50017
- 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;
50018
50065
  }
50019
- await this._persistState();
50020
- console.log(
50021
- `[SecureChannel] Room session ${convId.slice(0, 8)}... re-activated after ratchet re-init`
50022
- );
50023
50066
  } catch (reinitErr) {
50024
50067
  console.error(
50025
50068
  `[SecureChannel] Room ratchet re-init failed for conv ${convId.slice(0, 8)}...:`,
@@ -50198,13 +50241,14 @@ ${messageText}`;
50198
50241
  const dist = chain.getDistribution(this._deviceId);
50199
50242
  const distJson = JSON.stringify(dist);
50200
50243
  const alreadyDistributed = new Set(room.distributedTo ?? []);
50244
+ const pendingWsSends = [];
50201
50245
  for (const convId of room.conversationIds) {
50202
50246
  const session = this._sessions.get(convId);
50203
50247
  if (!session || alreadyDistributed.has(session.ownerDeviceId)) continue;
50204
50248
  const encrypted = session.ratchet.encrypt(distJson);
50205
50249
  const transport = encryptedMessageToTransport(encrypted);
50206
50250
  if (this._state === "ready" && this._ws) {
50207
- this._ws.send(
50251
+ pendingWsSends.push(
50208
50252
  JSON.stringify({
50209
50253
  event: "sender_key_distribution",
50210
50254
  data: {
@@ -50220,6 +50264,9 @@ ${messageText}`;
50220
50264
  }
50221
50265
  room.distributedTo = [...alreadyDistributed];
50222
50266
  await this._persistState();
50267
+ for (const frame of pendingWsSends) {
50268
+ this._ws.send(frame);
50269
+ }
50223
50270
  }
50224
50271
  /**
50225
50272
  * Handle an incoming sender_key_distribution event.
@@ -50417,8 +50464,10 @@ ${messageText}`;
50417
50464
  try {
50418
50465
  a2aPlaintext = ratchet.decrypt(encryptedMessage);
50419
50466
  } catch (decryptErr) {
50420
- console.error(`[SecureChannel] A2A decrypt failed \u2014 restoring ratchet state:`, decryptErr);
50467
+ console.error(`[SecureChannel] A2A decrypt failed \u2014 restoring ratchet, initiating rekey:`, decryptErr);
50421
50468
  channelEntry.session.ratchetState = ratchetSnapshot;
50469
+ await this._persistState();
50470
+ await this._initiateA2ARekey(channelId);
50422
50471
  return;
50423
50472
  }
50424
50473
  channelEntry.session.ratchetState = ratchet.serialize();
@@ -50460,24 +50509,9 @@ ${messageText}`;
50460
50509
  }
50461
50510
  } else {
50462
50511
  console.warn(
50463
- `[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`
50464
50513
  );
50465
- const entry = this._persisted?.a2aChannels?.[channelId];
50466
- if (entry && !entry.pendingEphemeralPrivateKey && !entry.session) {
50467
- try {
50468
- const a2aEphemeral = await generateEphemeralKeypair();
50469
- const ephPubHex = bytesToHex(a2aEphemeral.publicKey);
50470
- entry.pendingEphemeralPrivateKey = bytesToHex(a2aEphemeral.privateKey);
50471
- this._ws?.send(JSON.stringify({
50472
- event: "a2a_key_exchange",
50473
- data: { channel_id: channelId, ephemeral_key: ephPubHex }
50474
- }));
50475
- await this._persistState();
50476
- console.log(`[SecureChannel] On-demand A2A key exchange for ${channelId.slice(0, 8)}`);
50477
- } catch (kxErr) {
50478
- console.warn(`[SecureChannel] On-demand key exchange failed:`, kxErr);
50479
- }
50480
- }
50514
+ await this._initiateA2ARekey(channelId);
50481
50515
  }
50482
50516
  } else {
50483
50517
  const a2aMsg = {
@@ -50494,6 +50528,51 @@ ${messageText}`;
50494
50528
  }
50495
50529
  }
50496
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
+ }
50497
50576
  /**
50498
50577
  * Paginated sync: fetch missed messages in pages of 200, up to 5 pages (1000 messages).
50499
50578
  * Tracks message IDs in _syncMessageIds to prevent duplicate processing from concurrent WS messages.
@@ -50914,6 +50993,8 @@ ${messageText}`;
50914
50993
  const hasA2AChannels = !!this._persisted.a2aChannels && Object.keys(this._persisted.a2aChannels).length > 0;
50915
50994
  if (!hasOwnerSessions && !hasA2AChannels) return;
50916
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);
50917
50998
  for (const [convId, session] of this._sessions) {
50918
50999
  this._persisted.sessions[convId] = {
50919
51000
  ownerDeviceId: session.ownerDeviceId,