@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/index.js CHANGED
@@ -44743,7 +44743,7 @@ var init_ratchet = __esm({
44743
44743
  async "../crypto/dist/ratchet.js"() {
44744
44744
  "use strict";
44745
44745
  await init_libsodium_wrappers();
44746
- MAX_SKIP = 100;
44746
+ MAX_SKIP = 1e3;
44747
44747
  CHAIN_KEY_SEED = new Uint8Array([1]);
44748
44748
  MSG_KEY_SEED = new Uint8Array([2]);
44749
44749
  HEADER_KEY_SEED = new Uint8Array([3]);
@@ -47236,6 +47236,12 @@ var init_channel = __esm({
47236
47236
  this._primaryConversationId = this._persisted.primaryConversationId;
47237
47237
  this._fingerprint = this._persisted.fingerprint;
47238
47238
  this._lastInboundRoomId = this._persisted.lastInboundRoomId;
47239
+ if (this._persisted.seenMessageIds) {
47240
+ this._seenMessageIds = new Set(this._persisted.seenMessageIds.slice(-_SecureChannel.SEEN_MSG_MAX));
47241
+ }
47242
+ if (this._persisted.seenA2AMessageIds) {
47243
+ this._a2aSeenMessageIds = new Set(this._persisted.seenA2AMessageIds.slice(-_SecureChannel.A2A_SEEN_MAX));
47244
+ }
47239
47245
  for (const [convId, sessionData] of Object.entries(
47240
47246
  this._persisted.sessions
47241
47247
  )) {
@@ -47382,6 +47388,7 @@ var init_channel = __esm({
47382
47388
  }
47383
47389
  const messageGroupId = randomUUID2();
47384
47390
  let sentCount = 0;
47391
+ const pendingWsSends = [];
47385
47392
  for (const [convId, session] of this._sessions) {
47386
47393
  if (!session.activated) continue;
47387
47394
  if (roomConvIds.has(convId)) continue;
@@ -47414,7 +47421,7 @@ var init_channel = __esm({
47414
47421
  if (this._persisted?.hubId) {
47415
47422
  payload.sender_hub_id = this._persisted.hubId;
47416
47423
  }
47417
- this._ws.send(
47424
+ pendingWsSends.push(
47418
47425
  JSON.stringify({
47419
47426
  event: "message",
47420
47427
  data: payload
@@ -47440,6 +47447,9 @@ var init_channel = __esm({
47440
47447
  console.warn("[SecureChannel] send() delivered to 0 sessions (all skipped or failed)");
47441
47448
  }
47442
47449
  await this._persistState();
47450
+ for (const frame of pendingWsSends) {
47451
+ this._ws.send(frame);
47452
+ }
47443
47453
  }
47444
47454
  /**
47445
47455
  * Send a typing indicator to all owner devices.
@@ -47718,6 +47728,7 @@ var init_channel = __esm({
47718
47728
  if (recipients.length === 0) {
47719
47729
  throw new Error("No active sessions in room");
47720
47730
  }
47731
+ await this._persistState();
47721
47732
  if (this._state === "ready" && this._ws) {
47722
47733
  this._ws.send(
47723
47734
  JSON.stringify({
@@ -47757,7 +47768,6 @@ var init_channel = __esm({
47757
47768
  throw new Error(`Failed to send room message: ${err}`);
47758
47769
  }
47759
47770
  }
47760
- await this._persistState();
47761
47771
  }
47762
47772
  /**
47763
47773
  * Leave a room: remove sessions and persisted room state.
@@ -47856,11 +47866,12 @@ var init_channel = __esm({
47856
47866
  attachment: attachMeta
47857
47867
  });
47858
47868
  const messageGroupId = randomUUID2();
47869
+ const pendingWsSends = [];
47859
47870
  for (const [convId, session] of this._sessions) {
47860
47871
  if (!session.activated) continue;
47861
47872
  const encrypted = session.ratchet.encrypt(envelope);
47862
47873
  const transport = encryptedMessageToTransport(encrypted);
47863
- this._ws.send(
47874
+ pendingWsSends.push(
47864
47875
  JSON.stringify({
47865
47876
  event: "message",
47866
47877
  data: {
@@ -47874,6 +47885,9 @@ var init_channel = __esm({
47874
47885
  );
47875
47886
  }
47876
47887
  await this._persistState();
47888
+ for (const frame of pendingWsSends) {
47889
+ this._ws.send(frame);
47890
+ }
47877
47891
  }
47878
47892
  async sendActionConfirmation(confirmation) {
47879
47893
  const envelope = {
@@ -49690,11 +49704,12 @@ ${messageText}`;
49690
49704
  });
49691
49705
  this._appendHistory("agent", plaintext, topicId);
49692
49706
  const messageGroupId = randomUUID2();
49707
+ const pendingWsSends = [];
49693
49708
  for (const [convId, session] of this._sessions) {
49694
49709
  if (!session.activated) continue;
49695
49710
  const encrypted = session.ratchet.encrypt(envelope);
49696
49711
  const transport = encryptedMessageToTransport(encrypted);
49697
- this._ws.send(
49712
+ pendingWsSends.push(
49698
49713
  JSON.stringify({
49699
49714
  event: "message",
49700
49715
  data: {
@@ -49708,6 +49723,9 @@ ${messageText}`;
49708
49723
  );
49709
49724
  }
49710
49725
  await this._persistState();
49726
+ for (const frame of pendingWsSends) {
49727
+ this._ws.send(frame);
49728
+ }
49711
49729
  }
49712
49730
  /**
49713
49731
  * Relay an owner's message to all sibling sessions as encrypted sync messages.
@@ -49730,13 +49748,14 @@ ${messageText}`;
49730
49748
  ts: (/* @__PURE__ */ new Date()).toISOString(),
49731
49749
  topicId
49732
49750
  });
49751
+ const pendingWsSends = [];
49733
49752
  for (const [siblingConvId, siblingSession] of this._sessions) {
49734
49753
  if (siblingConvId === sourceConvId) continue;
49735
49754
  if (!siblingSession.activated) continue;
49736
49755
  if (roomConvIds.has(siblingConvId)) continue;
49737
49756
  const syncEncrypted = siblingSession.ratchet.encrypt(syncPayload);
49738
49757
  const syncTransport = encryptedMessageToTransport(syncEncrypted);
49739
- this._ws.send(
49758
+ pendingWsSends.push(
49740
49759
  JSON.stringify({
49741
49760
  event: "message",
49742
49761
  data: {
@@ -49747,6 +49766,10 @@ ${messageText}`;
49747
49766
  })
49748
49767
  );
49749
49768
  }
49769
+ await this._persistState();
49770
+ for (const frame of pendingWsSends) {
49771
+ this._ws.send(frame);
49772
+ }
49750
49773
  }
49751
49774
  /**
49752
49775
  * Resolve the agent's workspace directory.
@@ -49790,6 +49813,7 @@ ${messageText}`;
49790
49813
  const plaintext = JSON.stringify(payload);
49791
49814
  const encrypted = session.ratchet.encrypt(plaintext);
49792
49815
  const transport = encryptedMessageToTransport(encrypted);
49816
+ await this._persistState();
49793
49817
  this._ws.send(
49794
49818
  JSON.stringify({
49795
49819
  event: "message",
@@ -49800,7 +49824,6 @@ ${messageText}`;
49800
49824
  }
49801
49825
  })
49802
49826
  );
49803
- await this._persistState();
49804
49827
  }
49805
49828
  /**
49806
49829
  * Send stored message history to a newly-activated session.
@@ -49823,6 +49846,7 @@ ${messageText}`;
49823
49846
  });
49824
49847
  const encrypted = session.ratchet.encrypt(replayPayload);
49825
49848
  const transport = encryptedMessageToTransport(encrypted);
49849
+ await this._persistState();
49826
49850
  this._ws.send(
49827
49851
  JSON.stringify({
49828
49852
  event: "message",
@@ -50075,15 +50099,31 @@ ${messageText}`;
50075
50099
  console.log(
50076
50100
  `[SecureChannel] Room ratchet re-initialized for conv ${convId.slice(0, 8)}...`
50077
50101
  );
50078
- plaintext = session.ratchet.decrypt(encrypted);
50079
- session.activated = true;
50080
- if (this._persisted.sessions[convId]) {
50081
- this._persisted.sessions[convId].activated = true;
50102
+ const incomingMsgNum = encrypted.header.messageNumber;
50103
+ if (incomingMsgNum <= 5) {
50104
+ try {
50105
+ plaintext = session.ratchet.decrypt(encrypted);
50106
+ session.activated = true;
50107
+ if (this._persisted.sessions[convId]) {
50108
+ this._persisted.sessions[convId].activated = true;
50109
+ }
50110
+ await this._persistState();
50111
+ console.log(
50112
+ `[SecureChannel] Room session ${convId.slice(0, 8)}... re-activated after ratchet re-init`
50113
+ );
50114
+ } catch (retryErr) {
50115
+ console.warn(
50116
+ `[SecureChannel] Room re-init retry failed for conv ${convId.slice(0, 8)} (msgNum=${incomingMsgNum}):`,
50117
+ retryErr
50118
+ );
50119
+ return;
50120
+ }
50121
+ } else {
50122
+ console.log(
50123
+ `[SecureChannel] Room re-init: skipping message with msgNum=${incomingMsgNum} for conv ${convId.slice(0, 8)} (too far ahead for fresh ratchet)`
50124
+ );
50125
+ return;
50082
50126
  }
50083
- await this._persistState();
50084
- console.log(
50085
- `[SecureChannel] Room session ${convId.slice(0, 8)}... re-activated after ratchet re-init`
50086
- );
50087
50127
  } catch (reinitErr) {
50088
50128
  console.error(
50089
50129
  `[SecureChannel] Room ratchet re-init failed for conv ${convId.slice(0, 8)}...:`,
@@ -50262,13 +50302,14 @@ ${messageText}`;
50262
50302
  const dist = chain.getDistribution(this._deviceId);
50263
50303
  const distJson = JSON.stringify(dist);
50264
50304
  const alreadyDistributed = new Set(room.distributedTo ?? []);
50305
+ const pendingWsSends = [];
50265
50306
  for (const convId of room.conversationIds) {
50266
50307
  const session = this._sessions.get(convId);
50267
50308
  if (!session || alreadyDistributed.has(session.ownerDeviceId)) continue;
50268
50309
  const encrypted = session.ratchet.encrypt(distJson);
50269
50310
  const transport = encryptedMessageToTransport(encrypted);
50270
50311
  if (this._state === "ready" && this._ws) {
50271
- this._ws.send(
50312
+ pendingWsSends.push(
50272
50313
  JSON.stringify({
50273
50314
  event: "sender_key_distribution",
50274
50315
  data: {
@@ -50284,6 +50325,9 @@ ${messageText}`;
50284
50325
  }
50285
50326
  room.distributedTo = [...alreadyDistributed];
50286
50327
  await this._persistState();
50328
+ for (const frame of pendingWsSends) {
50329
+ this._ws.send(frame);
50330
+ }
50287
50331
  }
50288
50332
  /**
50289
50333
  * Handle an incoming sender_key_distribution event.
@@ -50481,8 +50525,10 @@ ${messageText}`;
50481
50525
  try {
50482
50526
  a2aPlaintext = ratchet.decrypt(encryptedMessage);
50483
50527
  } catch (decryptErr) {
50484
- console.error(`[SecureChannel] A2A decrypt failed \u2014 restoring ratchet state:`, decryptErr);
50528
+ console.error(`[SecureChannel] A2A decrypt failed \u2014 restoring ratchet, initiating rekey:`, decryptErr);
50485
50529
  channelEntry.session.ratchetState = ratchetSnapshot;
50530
+ await this._persistState();
50531
+ await this._initiateA2ARekey(channelId);
50486
50532
  return;
50487
50533
  }
50488
50534
  channelEntry.session.ratchetState = ratchet.serialize();
@@ -50524,24 +50570,9 @@ ${messageText}`;
50524
50570
  }
50525
50571
  } else {
50526
50572
  console.warn(
50527
- `[SecureChannel] Received encrypted A2A but no session for ${channelId.slice(0, 8)} \u2014 triggering key exchange`
50573
+ `[SecureChannel] Received encrypted A2A but no session for ${channelId.slice(0, 8)} \u2014 initiating rekey`
50528
50574
  );
50529
- const entry = this._persisted?.a2aChannels?.[channelId];
50530
- if (entry && !entry.pendingEphemeralPrivateKey && !entry.session) {
50531
- try {
50532
- const a2aEphemeral = await generateEphemeralKeypair();
50533
- const ephPubHex = bytesToHex(a2aEphemeral.publicKey);
50534
- entry.pendingEphemeralPrivateKey = bytesToHex(a2aEphemeral.privateKey);
50535
- this._ws?.send(JSON.stringify({
50536
- event: "a2a_key_exchange",
50537
- data: { channel_id: channelId, ephemeral_key: ephPubHex }
50538
- }));
50539
- await this._persistState();
50540
- console.log(`[SecureChannel] On-demand A2A key exchange for ${channelId.slice(0, 8)}`);
50541
- } catch (kxErr) {
50542
- console.warn(`[SecureChannel] On-demand key exchange failed:`, kxErr);
50543
- }
50544
- }
50575
+ await this._initiateA2ARekey(channelId);
50545
50576
  }
50546
50577
  } else {
50547
50578
  const a2aMsg = {
@@ -50558,6 +50589,51 @@ ${messageText}`;
50558
50589
  }
50559
50590
  }
50560
50591
  }
50592
+ /**
50593
+ * Initiate A2A channel rekey after decrypt failure.
50594
+ * Calls the backend to reset channel to approved, clears local session,
50595
+ * and submits a fresh ephemeral key for key exchange.
50596
+ */
50597
+ async _initiateA2ARekey(channelId) {
50598
+ const entry = this._persisted?.a2aChannels?.[channelId];
50599
+ if (!entry || !this._deviceJwt || !this._ws) return;
50600
+ const now = Date.now();
50601
+ const lastRekey = entry._lastRekeyAttempt ?? 0;
50602
+ if (now - lastRekey < 3e4) return;
50603
+ entry._lastRekeyAttempt = now;
50604
+ console.log(`[SecureChannel] Initiating A2A rekey for ${channelId.slice(0, 8)}...`);
50605
+ try {
50606
+ const res = await fetch(
50607
+ `${this.config.apiUrl}/api/v1/a2a/channels/${channelId}/rekey`,
50608
+ {
50609
+ method: "POST",
50610
+ headers: { Authorization: `Bearer ${this._deviceJwt}` }
50611
+ }
50612
+ );
50613
+ if (!res.ok) {
50614
+ const detail = await res.text();
50615
+ console.warn(`[SecureChannel] A2A rekey API failed (${res.status}): ${detail}`);
50616
+ return;
50617
+ }
50618
+ delete entry.session;
50619
+ delete entry.observerSession;
50620
+ delete entry.pendingEphemeralPrivateKey;
50621
+ delete entry.pendingObserverEphemeralPrivateKey;
50622
+ const a2aEphemeral = await generateEphemeralKeypair();
50623
+ entry.pendingEphemeralPrivateKey = bytesToHex(a2aEphemeral.privateKey);
50624
+ this._ws.send(JSON.stringify({
50625
+ event: "a2a_key_exchange",
50626
+ data: {
50627
+ channel_id: channelId,
50628
+ ephemeral_key: bytesToHex(a2aEphemeral.publicKey)
50629
+ }
50630
+ }));
50631
+ await this._persistState();
50632
+ console.log(`[SecureChannel] A2A rekey submitted for ${channelId.slice(0, 8)}`);
50633
+ } catch (err) {
50634
+ console.warn(`[SecureChannel] A2A rekey failed:`, err);
50635
+ }
50636
+ }
50561
50637
  /**
50562
50638
  * Paginated sync: fetch missed messages in pages of 200, up to 5 pages (1000 messages).
50563
50639
  * Tracks message IDs in _syncMessageIds to prevent duplicate processing from concurrent WS messages.
@@ -50978,6 +51054,8 @@ ${messageText}`;
50978
51054
  const hasA2AChannels = !!this._persisted.a2aChannels && Object.keys(this._persisted.a2aChannels).length > 0;
50979
51055
  if (!hasOwnerSessions && !hasA2AChannels) return;
50980
51056
  this._persisted.lastInboundRoomId = this._lastInboundRoomId;
51057
+ this._persisted.seenMessageIds = [...this._seenMessageIds].slice(-_SecureChannel.SEEN_MSG_MAX);
51058
+ this._persisted.seenA2AMessageIds = [...this._a2aSeenMessageIds].slice(-_SecureChannel.A2A_SEEN_MAX);
50981
51059
  for (const [convId, session] of this._sessions) {
50982
51060
  this._persisted.sessions[convId] = {
50983
51061
  ownerDeviceId: session.ownerDeviceId,