@agentvault/agentvault 0.19.26 → 0.19.27

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
@@ -47145,6 +47145,10 @@ var init_channel = __esm({
47145
47145
  /** Dedup buffer for A2A message IDs (prevents double-delivery via direct + Redis) */
47146
47146
  _a2aSeenMessageIds = /* @__PURE__ */ new Set();
47147
47147
  static A2A_SEEN_MAX = 500;
47148
+ /** Whether A2A channel sync has completed after (re)connect */
47149
+ _a2aSyncComplete = false;
47150
+ /** Messages buffered while A2A sync is in progress */
47151
+ _a2aBufferedMessages = [];
47148
47152
  /** Dedup buffer for regular message IDs (prevents double-decrypt via direct WS + Redis pub/sub) */
47149
47153
  _seenMessageIds = /* @__PURE__ */ new Set();
47150
47154
  static SEEN_MSG_MAX = 500;
@@ -48796,6 +48800,8 @@ var init_channel = __esm({
48796
48800
  ws.on("open", async () => {
48797
48801
  try {
48798
48802
  this._reconnectAttempt = 0;
48803
+ this._a2aSyncComplete = false;
48804
+ this._a2aBufferedMessages = [];
48799
48805
  this._lastWsOpenTime = Date.now();
48800
48806
  this._startPing(ws);
48801
48807
  this._startWakeDetector();
@@ -48841,8 +48847,21 @@ var init_channel = __esm({
48841
48847
  }, 3e3);
48842
48848
  }
48843
48849
  }
48850
+ this._a2aSyncComplete = true;
48851
+ if (this._a2aBufferedMessages.length > 0) {
48852
+ console.log(`[SecureChannel] Replaying ${this._a2aBufferedMessages.length} buffered A2A message(s)`);
48853
+ const buffered = this._a2aBufferedMessages.splice(0);
48854
+ for (const msg of buffered) {
48855
+ try {
48856
+ await this._handleA2AMessage(msg.data);
48857
+ } catch (err) {
48858
+ console.warn("[SecureChannel] Failed to replay buffered A2A message:", err);
48859
+ }
48860
+ }
48861
+ }
48844
48862
  } catch (err) {
48845
48863
  console.warn("[SecureChannel] A2A channel sync failed (non-fatal):", err);
48864
+ this._a2aSyncComplete = true;
48846
48865
  }
48847
48866
  if (!this._telemetryReporter && this._persisted?.deviceJwt && this._persisted?.hubId) {
48848
48867
  this._telemetryReporter = new TelemetryReporter({
@@ -49333,99 +49352,13 @@ var init_channel = __esm({
49333
49352
  this.emit("error", new Error(`Server: ${detail}`));
49334
49353
  }
49335
49354
  if (data.event === "a2a_message") {
49336
- const msgData = data.data || data;
49337
- const a2aMsgId = msgData.message_id;
49338
- if (a2aMsgId && this._a2aSeenMessageIds.has(a2aMsgId)) {
49355
+ if (!this._a2aSyncComplete) {
49356
+ const msgId = (data.data || data).message_id;
49357
+ console.log(`[SecureChannel] Buffering A2A message (sync not complete): ${msgId?.slice(0, 8)}`);
49358
+ this._a2aBufferedMessages.push({ data });
49339
49359
  return;
49340
49360
  }
49341
- if (a2aMsgId) {
49342
- this._a2aSeenMessageIds.add(a2aMsgId);
49343
- if (this._a2aSeenMessageIds.size > _SecureChannel.A2A_SEEN_MAX) {
49344
- const first = this._a2aSeenMessageIds.values().next().value;
49345
- if (first) this._a2aSeenMessageIds.delete(first);
49346
- }
49347
- }
49348
- if (msgData.ciphertext && msgData.header_blob) {
49349
- const channelId = msgData.channel_id || "";
49350
- const channelEntry = this._persisted?.a2aChannels?.[channelId];
49351
- if (channelEntry?.session?.ratchetState) {
49352
- try {
49353
- const headerJson = Buffer.from(msgData.header_blob, "hex").toString("utf-8");
49354
- const headerObj = JSON.parse(headerJson);
49355
- const encryptedMessage = {
49356
- header: {
49357
- dhPublicKey: hexToBytes(headerObj.dhPublicKey),
49358
- previousChainLength: headerObj.previousChainLength,
49359
- messageNumber: headerObj.messageNumber
49360
- },
49361
- headerSignature: msgData.header_signature ? hexToBytes(msgData.header_signature) : new Uint8Array(64),
49362
- ciphertext: hexToBytes(msgData.ciphertext),
49363
- nonce: hexToBytes(msgData.nonce)
49364
- };
49365
- const ratchet = DoubleRatchet.deserialize(channelEntry.session.ratchetState);
49366
- const ratchetSnapshot = channelEntry.session.ratchetState;
49367
- let a2aPlaintext;
49368
- try {
49369
- a2aPlaintext = ratchet.decrypt(encryptedMessage);
49370
- } catch (decryptErr) {
49371
- console.error(`[SecureChannel] A2A decrypt failed \u2014 restoring ratchet state:`, decryptErr);
49372
- channelEntry.session.ratchetState = ratchetSnapshot;
49373
- return;
49374
- }
49375
- channelEntry.session.ratchetState = ratchet.serialize();
49376
- if (channelEntry.role === "responder" && !channelEntry.session.activated) {
49377
- channelEntry.session.activated = true;
49378
- console.log(
49379
- `[SecureChannel] A2A responder ratchet activated for ${channelId.slice(0, 8)}...`
49380
- );
49381
- const queued = this._a2aPendingQueue[channelId];
49382
- if (queued && queued.length > 0) {
49383
- delete this._a2aPendingQueue[channelId];
49384
- console.log(`[SecureChannel] Flushing ${queued.length} queued A2A message(s)`);
49385
- await this._persistState();
49386
- for (const pending of queued) {
49387
- try {
49388
- await this.sendToAgent(channelEntry.hubAddress, pending.text, pending.opts);
49389
- } catch (flushErr) {
49390
- console.error("[SecureChannel] Failed to flush queued A2A message:", flushErr);
49391
- }
49392
- }
49393
- }
49394
- }
49395
- await this._persistState();
49396
- const a2aMsg = {
49397
- text: a2aPlaintext,
49398
- fromHubAddress: msgData.from_hub_address || msgData.hub_address || "",
49399
- channelId,
49400
- conversationId: msgData.conversation_id || "",
49401
- parentSpanId: msgData.parent_span_id,
49402
- timestamp: msgData.timestamp || (/* @__PURE__ */ new Date()).toISOString()
49403
- };
49404
- this.emit("a2a_message", a2aMsg);
49405
- if (this.config.onA2AMessage) {
49406
- this.config.onA2AMessage(a2aMsg);
49407
- }
49408
- } catch (decryptErr) {
49409
- console.error(`[SecureChannel] A2A decrypt failed for channel ${channelId.slice(0, 8)}...:`, decryptErr);
49410
- this.emit("error", decryptErr);
49411
- }
49412
- } else {
49413
- console.warn(`[SecureChannel] Received encrypted A2A message but no session for channel ${channelId}`);
49414
- }
49415
- } else {
49416
- const a2aMsg = {
49417
- text: msgData.plaintext || msgData.text,
49418
- fromHubAddress: msgData.from_hub_address || msgData.hub_address || "",
49419
- channelId: msgData.channel_id || "",
49420
- conversationId: msgData.conversation_id || "",
49421
- parentSpanId: msgData.parent_span_id,
49422
- timestamp: msgData.timestamp || (/* @__PURE__ */ new Date()).toISOString()
49423
- };
49424
- this.emit("a2a_message", a2aMsg);
49425
- if (this.config.onA2AMessage) {
49426
- this.config.onA2AMessage(a2aMsg);
49427
- }
49428
- }
49361
+ await this._handleA2AMessage(data);
49429
49362
  }
49430
49363
  } catch (err) {
49431
49364
  this.emit("error", err);
@@ -50504,6 +50437,123 @@ ${messageText}`;
50504
50437
  * Sync missed messages across ALL sessions.
50505
50438
  * For each conversation, fetches messages since last sync and decrypts.
50506
50439
  */
50440
+ /**
50441
+ * Handle an incoming A2A message (extracted from WS handler for reuse during buffer replay).
50442
+ * Includes dedup, encrypted path (with on-demand key exchange), and legacy plaintext path.
50443
+ */
50444
+ async _handleA2AMessage(data) {
50445
+ const msgData = data.data || data;
50446
+ const a2aMsgId = msgData.message_id;
50447
+ if (a2aMsgId && this._a2aSeenMessageIds.has(a2aMsgId)) {
50448
+ return;
50449
+ }
50450
+ if (a2aMsgId) {
50451
+ this._a2aSeenMessageIds.add(a2aMsgId);
50452
+ if (this._a2aSeenMessageIds.size > _SecureChannel.A2A_SEEN_MAX) {
50453
+ const first = this._a2aSeenMessageIds.values().next().value;
50454
+ if (first) this._a2aSeenMessageIds.delete(first);
50455
+ }
50456
+ }
50457
+ if (msgData.ciphertext && msgData.header_blob) {
50458
+ const channelId = msgData.channel_id || "";
50459
+ const channelEntry = this._persisted?.a2aChannels?.[channelId];
50460
+ if (channelEntry?.session?.ratchetState) {
50461
+ try {
50462
+ const headerJson = Buffer.from(msgData.header_blob, "hex").toString("utf-8");
50463
+ const headerObj = JSON.parse(headerJson);
50464
+ const encryptedMessage = {
50465
+ header: {
50466
+ dhPublicKey: hexToBytes(headerObj.dhPublicKey),
50467
+ previousChainLength: headerObj.previousChainLength,
50468
+ messageNumber: headerObj.messageNumber
50469
+ },
50470
+ headerSignature: msgData.header_signature ? hexToBytes(msgData.header_signature) : new Uint8Array(64),
50471
+ ciphertext: hexToBytes(msgData.ciphertext),
50472
+ nonce: hexToBytes(msgData.nonce)
50473
+ };
50474
+ const ratchet = DoubleRatchet.deserialize(channelEntry.session.ratchetState);
50475
+ const ratchetSnapshot = channelEntry.session.ratchetState;
50476
+ let a2aPlaintext;
50477
+ try {
50478
+ a2aPlaintext = ratchet.decrypt(encryptedMessage);
50479
+ } catch (decryptErr) {
50480
+ console.error(`[SecureChannel] A2A decrypt failed \u2014 restoring ratchet state:`, decryptErr);
50481
+ channelEntry.session.ratchetState = ratchetSnapshot;
50482
+ return;
50483
+ }
50484
+ channelEntry.session.ratchetState = ratchet.serialize();
50485
+ if (channelEntry.role === "responder" && !channelEntry.session.activated) {
50486
+ channelEntry.session.activated = true;
50487
+ console.log(
50488
+ `[SecureChannel] A2A responder ratchet activated for ${channelId.slice(0, 8)}...`
50489
+ );
50490
+ const queued = this._a2aPendingQueue[channelId];
50491
+ if (queued && queued.length > 0) {
50492
+ delete this._a2aPendingQueue[channelId];
50493
+ console.log(`[SecureChannel] Flushing ${queued.length} queued A2A message(s)`);
50494
+ await this._persistState();
50495
+ for (const pending of queued) {
50496
+ try {
50497
+ await this.sendToAgent(channelEntry.hubAddress, pending.text, pending.opts);
50498
+ } catch (flushErr) {
50499
+ console.error("[SecureChannel] Failed to flush queued A2A message:", flushErr);
50500
+ }
50501
+ }
50502
+ }
50503
+ }
50504
+ await this._persistState();
50505
+ const a2aMsg = {
50506
+ text: a2aPlaintext,
50507
+ fromHubAddress: msgData.from_hub_address || msgData.hub_address || "",
50508
+ channelId,
50509
+ conversationId: msgData.conversation_id || "",
50510
+ parentSpanId: msgData.parent_span_id,
50511
+ timestamp: msgData.timestamp || (/* @__PURE__ */ new Date()).toISOString()
50512
+ };
50513
+ this.emit("a2a_message", a2aMsg);
50514
+ if (this.config.onA2AMessage) {
50515
+ this.config.onA2AMessage(a2aMsg);
50516
+ }
50517
+ } catch (decryptErr) {
50518
+ console.error(`[SecureChannel] A2A decrypt failed for channel ${channelId.slice(0, 8)}...:`, decryptErr);
50519
+ this.emit("error", decryptErr);
50520
+ }
50521
+ } else {
50522
+ console.warn(
50523
+ `[SecureChannel] Received encrypted A2A but no session for ${channelId.slice(0, 8)} \u2014 triggering key exchange`
50524
+ );
50525
+ const entry = this._persisted?.a2aChannels?.[channelId];
50526
+ if (entry && !entry.pendingEphemeralPrivateKey && !entry.session) {
50527
+ try {
50528
+ const a2aEphemeral = await generateEphemeralKeypair();
50529
+ const ephPubHex = bytesToHex(a2aEphemeral.publicKey);
50530
+ entry.pendingEphemeralPrivateKey = bytesToHex(a2aEphemeral.privateKey);
50531
+ this._ws?.send(JSON.stringify({
50532
+ event: "a2a_key_exchange",
50533
+ data: { channel_id: channelId, ephemeral_key: ephPubHex }
50534
+ }));
50535
+ await this._persistState();
50536
+ console.log(`[SecureChannel] On-demand A2A key exchange for ${channelId.slice(0, 8)}`);
50537
+ } catch (kxErr) {
50538
+ console.warn(`[SecureChannel] On-demand key exchange failed:`, kxErr);
50539
+ }
50540
+ }
50541
+ }
50542
+ } else {
50543
+ const a2aMsg = {
50544
+ text: msgData.plaintext || msgData.text,
50545
+ fromHubAddress: msgData.from_hub_address || msgData.hub_address || "",
50546
+ channelId: msgData.channel_id || "",
50547
+ conversationId: msgData.conversation_id || "",
50548
+ parentSpanId: msgData.parent_span_id,
50549
+ timestamp: msgData.timestamp || (/* @__PURE__ */ new Date()).toISOString()
50550
+ };
50551
+ this.emit("a2a_message", a2aMsg);
50552
+ if (this.config.onA2AMessage) {
50553
+ this.config.onA2AMessage(a2aMsg);
50554
+ }
50555
+ }
50556
+ }
50507
50557
  /**
50508
50558
  * Paginated sync: fetch missed messages in pages of 200, up to 5 pages (1000 messages).
50509
50559
  * Tracks message IDs in _syncMessageIds to prevent duplicate processing from concurrent WS messages.