@agentvault/agentvault 0.13.9 → 0.13.10

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
@@ -44880,6 +44880,7 @@ var DoubleRatchet = class _DoubleRatchet {
44880
44880
  serialize() {
44881
44881
  const s2 = this.state;
44882
44882
  return JSON.stringify({
44883
+ version: 1,
44883
44884
  rootKey: libsodium_wrappers_default.to_hex(s2.rootKey),
44884
44885
  sendingChain: s2.sendingChain ? {
44885
44886
  chainKey: libsodium_wrappers_default.to_hex(s2.sendingChain.chainKey),
@@ -44907,33 +44908,59 @@ var DoubleRatchet = class _DoubleRatchet {
44907
44908
  });
44908
44909
  }
44909
44910
  static deserialize(json) {
44910
- const d2 = JSON.parse(json);
44911
- return new _DoubleRatchet({
44912
- rootKey: libsodium_wrappers_default.from_hex(d2.rootKey),
44913
- sendingChain: d2.sendingChain ? {
44914
- chainKey: libsodium_wrappers_default.from_hex(d2.sendingChain.chainKey),
44915
- messageNumber: d2.sendingChain.messageNumber
44916
- } : null,
44917
- receivingChain: d2.receivingChain ? {
44918
- chainKey: libsodium_wrappers_default.from_hex(d2.receivingChain.chainKey),
44919
- messageNumber: d2.receivingChain.messageNumber
44920
- } : null,
44921
- dhSendingKeypair: {
44922
- publicKey: libsodium_wrappers_default.from_hex(d2.dhSendingKeypair.publicKey),
44923
- privateKey: libsodium_wrappers_default.from_hex(d2.dhSendingKeypair.privateKey)
44924
- },
44925
- dhReceivingPublicKey: d2.dhReceivingPublicKey ? libsodium_wrappers_default.from_hex(d2.dhReceivingPublicKey) : null,
44926
- identityKeypair: {
44927
- publicKey: libsodium_wrappers_default.from_hex(d2.identityKeypair.publicKey),
44928
- privateKey: libsodium_wrappers_default.from_hex(d2.identityKeypair.privateKey)
44929
- },
44930
- previousSendingChainLength: d2.previousSendingChainLength,
44931
- skippedKeys: d2.skippedKeys.map((sk) => ({
44932
- dhPub: sk.dhPub,
44933
- n: sk.n,
44934
- messageKey: libsodium_wrappers_default.from_hex(sk.messageKey)
44935
- }))
44936
- });
44911
+ let d2;
44912
+ try {
44913
+ d2 = JSON.parse(json);
44914
+ } catch {
44915
+ throw new Error("Ratchet state: corrupt JSON");
44916
+ }
44917
+ if (d2.version !== void 0 && d2.version !== 1) {
44918
+ throw new Error(`Ratchet state version ${d2.version} not supported`);
44919
+ }
44920
+ if (typeof d2.rootKey !== "string") {
44921
+ throw new Error("Ratchet state: missing required field rootKey");
44922
+ }
44923
+ const dhSend = d2.dhSendingKeypair;
44924
+ if (!dhSend || typeof dhSend.publicKey !== "string" || typeof dhSend.privateKey !== "string") {
44925
+ throw new Error("Ratchet state: missing required field dhSendingKeypair");
44926
+ }
44927
+ const idKp = d2.identityKeypair;
44928
+ if (!idKp || typeof idKp.publicKey !== "string" || typeof idKp.privateKey !== "string") {
44929
+ throw new Error("Ratchet state: missing required field identityKeypair");
44930
+ }
44931
+ try {
44932
+ return new _DoubleRatchet({
44933
+ rootKey: libsodium_wrappers_default.from_hex(d2.rootKey),
44934
+ sendingChain: d2.sendingChain ? {
44935
+ chainKey: libsodium_wrappers_default.from_hex(d2.sendingChain.chainKey),
44936
+ messageNumber: d2.sendingChain.messageNumber
44937
+ } : null,
44938
+ receivingChain: d2.receivingChain ? {
44939
+ chainKey: libsodium_wrappers_default.from_hex(d2.receivingChain.chainKey),
44940
+ messageNumber: d2.receivingChain.messageNumber
44941
+ } : null,
44942
+ dhSendingKeypair: {
44943
+ publicKey: libsodium_wrappers_default.from_hex(dhSend.publicKey),
44944
+ privateKey: libsodium_wrappers_default.from_hex(dhSend.privateKey)
44945
+ },
44946
+ dhReceivingPublicKey: d2.dhReceivingPublicKey ? libsodium_wrappers_default.from_hex(d2.dhReceivingPublicKey) : null,
44947
+ identityKeypair: {
44948
+ publicKey: libsodium_wrappers_default.from_hex(idKp.publicKey),
44949
+ privateKey: libsodium_wrappers_default.from_hex(idKp.privateKey)
44950
+ },
44951
+ previousSendingChainLength: d2.previousSendingChainLength,
44952
+ skippedKeys: d2.skippedKeys.map((sk) => ({
44953
+ dhPub: sk.dhPub,
44954
+ n: sk.n,
44955
+ messageKey: libsodium_wrappers_default.from_hex(sk.messageKey)
44956
+ }))
44957
+ });
44958
+ } catch (err) {
44959
+ if (err instanceof Error && err.message.startsWith("Ratchet state")) {
44960
+ throw err;
44961
+ }
44962
+ throw new Error(`Ratchet state: corrupt hex data \u2014 ${err instanceof Error ? err.message : String(err)}`);
44963
+ }
44937
44964
  }
44938
44965
  };
44939
44966
 
@@ -45632,6 +45659,8 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45632
45659
  _telemetryReporter = null;
45633
45660
  /** Topic ID from the most recent inbound message — used as fallback for replies. */
45634
45661
  _lastIncomingTopicId;
45662
+ /** Rate-limit: last resync_request timestamp per conversation (5-min cooldown). */
45663
+ _lastResyncRequest = /* @__PURE__ */ new Map();
45635
45664
  // Liveness detection: server sends app-level {"event":"ping"} every 30s.
45636
45665
  // We check every 30s; if no data received in 90s (3 missed pings), connection is dead.
45637
45666
  static PING_INTERVAL_MS = 3e4;
@@ -46890,6 +46919,13 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
46890
46919
  await this._handleDeviceLinked(data.data);
46891
46920
  return;
46892
46921
  }
46922
+ if (data.event === "resync_request") {
46923
+ await this._handleResyncRequest(data.data);
46924
+ return;
46925
+ }
46926
+ if (data.event === "resync_ack") {
46927
+ return;
46928
+ }
46893
46929
  if (data.event === "message") {
46894
46930
  try {
46895
46931
  await this._handleIncomingMessage(data.data);
@@ -47295,6 +47331,26 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
47295
47331
  } catch (restoreErr) {
47296
47332
  console.error("[SecureChannel] Ratchet restore failed:", restoreErr);
47297
47333
  }
47334
+ const RESYNC_COOLDOWN_MS = 5 * 60 * 1e3;
47335
+ const lastResync = this._lastResyncRequest.get(convId) ?? 0;
47336
+ if (Date.now() - lastResync > RESYNC_COOLDOWN_MS && this._ws && this._persisted) {
47337
+ this._lastResyncRequest.set(convId, Date.now());
47338
+ const idPubHex = this._persisted.identityKeypair.publicKey;
47339
+ const ephPubHex = this._persisted.ephemeralKeypair.publicKey;
47340
+ this._ws.send(
47341
+ JSON.stringify({
47342
+ event: "resync_request",
47343
+ data: {
47344
+ conversation_id: convId,
47345
+ reason: "decrypt_failure",
47346
+ identity_public_key: idPubHex,
47347
+ ephemeral_public_key: ephPubHex
47348
+ }
47349
+ })
47350
+ );
47351
+ console.log(`[SecureChannel] Sent resync_request for conv ${convId.slice(0, 8)}...`);
47352
+ this.emit("resync_requested", { conversationId: convId, reason: "decrypt_failure" });
47353
+ }
47298
47354
  return;
47299
47355
  }
47300
47356
  this._sendAck(msgData.message_id);
@@ -47619,6 +47675,70 @@ ${messageText}`;
47619
47675
  this.emit("error", err);
47620
47676
  }
47621
47677
  }
47678
+ /**
47679
+ * Handle a resync_request from the owner (owner-initiated ratchet re-establishment).
47680
+ * Re-derives shared secret via X3DH as responder, initializes fresh receiver ratchet,
47681
+ * and sends resync_ack back with agent's public keys.
47682
+ */
47683
+ async _handleResyncRequest(data) {
47684
+ const convId = data.conversation_id;
47685
+ console.log(
47686
+ `[SecureChannel] Received resync_request for conv ${convId.slice(0, 8)}... (reason: ${data.reason ?? "unknown"})`
47687
+ );
47688
+ try {
47689
+ if (!this._persisted) {
47690
+ console.error("[SecureChannel] Cannot handle resync \u2014 no persisted state");
47691
+ return;
47692
+ }
47693
+ const identity = this._persisted.identityKeypair;
47694
+ const ephemeral = this._persisted.ephemeralKeypair;
47695
+ const sharedSecret = performX3DH({
47696
+ myIdentityPrivate: hexToBytes(identity.privateKey),
47697
+ myEphemeralPrivate: hexToBytes(ephemeral.privateKey),
47698
+ theirIdentityPublic: hexToBytes(data.identity_public_key),
47699
+ theirEphemeralPublic: hexToBytes(data.ephemeral_public_key),
47700
+ isInitiator: false
47701
+ });
47702
+ const ratchet = DoubleRatchet.initReceiver(sharedSecret, {
47703
+ publicKey: hexToBytes(identity.publicKey),
47704
+ privateKey: hexToBytes(identity.privateKey),
47705
+ keyType: "ed25519"
47706
+ });
47707
+ const existingSession = this._sessions.get(convId);
47708
+ const ownerDeviceId = data.sender_device_id ?? existingSession?.ownerDeviceId ?? "";
47709
+ this._sessions.set(convId, {
47710
+ ownerDeviceId,
47711
+ ratchet,
47712
+ activated: false
47713
+ // Wait for owner's encrypted session_init
47714
+ });
47715
+ this._persisted.sessions[convId] = {
47716
+ ownerDeviceId,
47717
+ ratchetState: ratchet.serialize(),
47718
+ activated: false
47719
+ };
47720
+ await this._persistState();
47721
+ if (this._ws) {
47722
+ this._ws.send(
47723
+ JSON.stringify({
47724
+ event: "resync_ack",
47725
+ data: {
47726
+ conversation_id: convId,
47727
+ identity_public_key: identity.publicKey,
47728
+ ephemeral_public_key: ephemeral.publicKey
47729
+ }
47730
+ })
47731
+ );
47732
+ }
47733
+ console.log(
47734
+ `[SecureChannel] Resync complete for conv ${convId.slice(0, 8)}... \u2014 waiting for owner session_init`
47735
+ );
47736
+ this.emit("resync_completed", { conversationId: convId });
47737
+ } catch (err) {
47738
+ console.error(`[SecureChannel] Resync failed for conv ${convId.slice(0, 8)}...:`, err);
47739
+ this.emit("error", err);
47740
+ }
47741
+ }
47622
47742
  /**
47623
47743
  * Handle an incoming room message. Finds the pairwise conversation
47624
47744
  * for the sender, decrypts, and emits a room_message event.