@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/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 = {
@@ -48885,6 +48899,9 @@ var init_channel = __esm({
48885
48899
  this._lastWakeTick = Date.now();
48886
48900
  try {
48887
48901
  const data = JSON.parse(raw.toString());
48902
+ if (data.event && data.event !== "ping" && data.event !== "typing") {
48903
+ console.log(`[SecureChannel] WS event: ${data.event} conv=${(data.conversation_id || data.data?.conversation_id || "").toString().slice(0, 8)}`);
48904
+ }
48888
48905
  if (data.event === "ping") {
48889
48906
  ws.send(JSON.stringify({ event: "pong" }));
48890
48907
  return;
@@ -49687,11 +49704,12 @@ ${messageText}`;
49687
49704
  });
49688
49705
  this._appendHistory("agent", plaintext, topicId);
49689
49706
  const messageGroupId = randomUUID2();
49707
+ const pendingWsSends = [];
49690
49708
  for (const [convId, session] of this._sessions) {
49691
49709
  if (!session.activated) continue;
49692
49710
  const encrypted = session.ratchet.encrypt(envelope);
49693
49711
  const transport = encryptedMessageToTransport(encrypted);
49694
- this._ws.send(
49712
+ pendingWsSends.push(
49695
49713
  JSON.stringify({
49696
49714
  event: "message",
49697
49715
  data: {
@@ -49705,6 +49723,9 @@ ${messageText}`;
49705
49723
  );
49706
49724
  }
49707
49725
  await this._persistState();
49726
+ for (const frame of pendingWsSends) {
49727
+ this._ws.send(frame);
49728
+ }
49708
49729
  }
49709
49730
  /**
49710
49731
  * Relay an owner's message to all sibling sessions as encrypted sync messages.
@@ -49727,13 +49748,14 @@ ${messageText}`;
49727
49748
  ts: (/* @__PURE__ */ new Date()).toISOString(),
49728
49749
  topicId
49729
49750
  });
49751
+ const pendingWsSends = [];
49730
49752
  for (const [siblingConvId, siblingSession] of this._sessions) {
49731
49753
  if (siblingConvId === sourceConvId) continue;
49732
49754
  if (!siblingSession.activated) continue;
49733
49755
  if (roomConvIds.has(siblingConvId)) continue;
49734
49756
  const syncEncrypted = siblingSession.ratchet.encrypt(syncPayload);
49735
49757
  const syncTransport = encryptedMessageToTransport(syncEncrypted);
49736
- this._ws.send(
49758
+ pendingWsSends.push(
49737
49759
  JSON.stringify({
49738
49760
  event: "message",
49739
49761
  data: {
@@ -49744,6 +49766,10 @@ ${messageText}`;
49744
49766
  })
49745
49767
  );
49746
49768
  }
49769
+ await this._persistState();
49770
+ for (const frame of pendingWsSends) {
49771
+ this._ws.send(frame);
49772
+ }
49747
49773
  }
49748
49774
  /**
49749
49775
  * Resolve the agent's workspace directory.
@@ -49787,6 +49813,7 @@ ${messageText}`;
49787
49813
  const plaintext = JSON.stringify(payload);
49788
49814
  const encrypted = session.ratchet.encrypt(plaintext);
49789
49815
  const transport = encryptedMessageToTransport(encrypted);
49816
+ await this._persistState();
49790
49817
  this._ws.send(
49791
49818
  JSON.stringify({
49792
49819
  event: "message",
@@ -49797,7 +49824,6 @@ ${messageText}`;
49797
49824
  }
49798
49825
  })
49799
49826
  );
49800
- await this._persistState();
49801
49827
  }
49802
49828
  /**
49803
49829
  * Send stored message history to a newly-activated session.
@@ -49820,6 +49846,7 @@ ${messageText}`;
49820
49846
  });
49821
49847
  const encrypted = session.ratchet.encrypt(replayPayload);
49822
49848
  const transport = encryptedMessageToTransport(encrypted);
49849
+ await this._persistState();
49823
49850
  this._ws.send(
49824
49851
  JSON.stringify({
49825
49852
  event: "message",
@@ -50072,15 +50099,31 @@ ${messageText}`;
50072
50099
  console.log(
50073
50100
  `[SecureChannel] Room ratchet re-initialized for conv ${convId.slice(0, 8)}...`
50074
50101
  );
50075
- plaintext = session.ratchet.decrypt(encrypted);
50076
- session.activated = true;
50077
- if (this._persisted.sessions[convId]) {
50078
- 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;
50079
50126
  }
50080
- await this._persistState();
50081
- console.log(
50082
- `[SecureChannel] Room session ${convId.slice(0, 8)}... re-activated after ratchet re-init`
50083
- );
50084
50127
  } catch (reinitErr) {
50085
50128
  console.error(
50086
50129
  `[SecureChannel] Room ratchet re-init failed for conv ${convId.slice(0, 8)}...:`,
@@ -50259,13 +50302,14 @@ ${messageText}`;
50259
50302
  const dist = chain.getDistribution(this._deviceId);
50260
50303
  const distJson = JSON.stringify(dist);
50261
50304
  const alreadyDistributed = new Set(room.distributedTo ?? []);
50305
+ const pendingWsSends = [];
50262
50306
  for (const convId of room.conversationIds) {
50263
50307
  const session = this._sessions.get(convId);
50264
50308
  if (!session || alreadyDistributed.has(session.ownerDeviceId)) continue;
50265
50309
  const encrypted = session.ratchet.encrypt(distJson);
50266
50310
  const transport = encryptedMessageToTransport(encrypted);
50267
50311
  if (this._state === "ready" && this._ws) {
50268
- this._ws.send(
50312
+ pendingWsSends.push(
50269
50313
  JSON.stringify({
50270
50314
  event: "sender_key_distribution",
50271
50315
  data: {
@@ -50281,6 +50325,9 @@ ${messageText}`;
50281
50325
  }
50282
50326
  room.distributedTo = [...alreadyDistributed];
50283
50327
  await this._persistState();
50328
+ for (const frame of pendingWsSends) {
50329
+ this._ws.send(frame);
50330
+ }
50284
50331
  }
50285
50332
  /**
50286
50333
  * Handle an incoming sender_key_distribution event.
@@ -50478,8 +50525,10 @@ ${messageText}`;
50478
50525
  try {
50479
50526
  a2aPlaintext = ratchet.decrypt(encryptedMessage);
50480
50527
  } catch (decryptErr) {
50481
- console.error(`[SecureChannel] A2A decrypt failed \u2014 restoring ratchet state:`, decryptErr);
50528
+ console.error(`[SecureChannel] A2A decrypt failed \u2014 restoring ratchet, initiating rekey:`, decryptErr);
50482
50529
  channelEntry.session.ratchetState = ratchetSnapshot;
50530
+ await this._persistState();
50531
+ await this._initiateA2ARekey(channelId);
50483
50532
  return;
50484
50533
  }
50485
50534
  channelEntry.session.ratchetState = ratchet.serialize();
@@ -50521,24 +50570,9 @@ ${messageText}`;
50521
50570
  }
50522
50571
  } else {
50523
50572
  console.warn(
50524
- `[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`
50525
50574
  );
50526
- const entry = this._persisted?.a2aChannels?.[channelId];
50527
- if (entry && !entry.pendingEphemeralPrivateKey && !entry.session) {
50528
- try {
50529
- const a2aEphemeral = await generateEphemeralKeypair();
50530
- const ephPubHex = bytesToHex(a2aEphemeral.publicKey);
50531
- entry.pendingEphemeralPrivateKey = bytesToHex(a2aEphemeral.privateKey);
50532
- this._ws?.send(JSON.stringify({
50533
- event: "a2a_key_exchange",
50534
- data: { channel_id: channelId, ephemeral_key: ephPubHex }
50535
- }));
50536
- await this._persistState();
50537
- console.log(`[SecureChannel] On-demand A2A key exchange for ${channelId.slice(0, 8)}`);
50538
- } catch (kxErr) {
50539
- console.warn(`[SecureChannel] On-demand key exchange failed:`, kxErr);
50540
- }
50541
- }
50575
+ await this._initiateA2ARekey(channelId);
50542
50576
  }
50543
50577
  } else {
50544
50578
  const a2aMsg = {
@@ -50555,6 +50589,51 @@ ${messageText}`;
50555
50589
  }
50556
50590
  }
50557
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
+ }
50558
50637
  /**
50559
50638
  * Paginated sync: fetch missed messages in pages of 200, up to 5 pages (1000 messages).
50560
50639
  * Tracks message IDs in _syncMessageIds to prevent duplicate processing from concurrent WS messages.
@@ -50975,6 +51054,8 @@ ${messageText}`;
50975
51054
  const hasA2AChannels = !!this._persisted.a2aChannels && Object.keys(this._persisted.a2aChannels).length > 0;
50976
51055
  if (!hasOwnerSessions && !hasA2AChannels) return;
50977
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);
50978
51059
  for (const [convId, session] of this._sessions) {
50979
51060
  this._persisted.sessions[convId] = {
50980
51061
  ownerDeviceId: session.ownerDeviceId,