@agentvault/agentvault 0.13.9 → 0.13.11

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);
@@ -47276,8 +47312,28 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
47276
47312
  const session = this._sessions.get(convId);
47277
47313
  if (!session) {
47278
47314
  console.warn(
47279
- `[SecureChannel] No session for conversation ${convId}, skipping`
47315
+ `[SecureChannel] No session for conversation ${convId}, requesting resync`
47280
47316
  );
47317
+ const RESYNC_COOLDOWN_MS = 5 * 60 * 1e3;
47318
+ const lastResync = this._lastResyncRequest.get(convId) ?? 0;
47319
+ if (Date.now() - lastResync > RESYNC_COOLDOWN_MS && this._ws && this._persisted) {
47320
+ this._lastResyncRequest.set(convId, Date.now());
47321
+ this._ws.send(
47322
+ JSON.stringify({
47323
+ event: "resync_request",
47324
+ data: {
47325
+ conversation_id: convId,
47326
+ reason: "no_session",
47327
+ identity_public_key: this._persisted.identityKeypair.publicKey,
47328
+ ephemeral_public_key: this._persisted.ephemeralKeypair.publicKey
47329
+ }
47330
+ })
47331
+ );
47332
+ this.emit("resync_requested", {
47333
+ conversationId: convId,
47334
+ reason: "no_session"
47335
+ });
47336
+ }
47281
47337
  return;
47282
47338
  }
47283
47339
  const encrypted = transportToEncryptedMessage({
@@ -47295,6 +47351,26 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
47295
47351
  } catch (restoreErr) {
47296
47352
  console.error("[SecureChannel] Ratchet restore failed:", restoreErr);
47297
47353
  }
47354
+ const RESYNC_COOLDOWN_MS = 5 * 60 * 1e3;
47355
+ const lastResync = this._lastResyncRequest.get(convId) ?? 0;
47356
+ if (Date.now() - lastResync > RESYNC_COOLDOWN_MS && this._ws && this._persisted) {
47357
+ this._lastResyncRequest.set(convId, Date.now());
47358
+ const idPubHex = this._persisted.identityKeypair.publicKey;
47359
+ const ephPubHex = this._persisted.ephemeralKeypair.publicKey;
47360
+ this._ws.send(
47361
+ JSON.stringify({
47362
+ event: "resync_request",
47363
+ data: {
47364
+ conversation_id: convId,
47365
+ reason: "decrypt_failure",
47366
+ identity_public_key: idPubHex,
47367
+ ephemeral_public_key: ephPubHex
47368
+ }
47369
+ })
47370
+ );
47371
+ console.log(`[SecureChannel] Sent resync_request for conv ${convId.slice(0, 8)}...`);
47372
+ this.emit("resync_requested", { conversationId: convId, reason: "decrypt_failure" });
47373
+ }
47298
47374
  return;
47299
47375
  }
47300
47376
  this._sendAck(msgData.message_id);
@@ -47619,6 +47695,70 @@ ${messageText}`;
47619
47695
  this.emit("error", err);
47620
47696
  }
47621
47697
  }
47698
+ /**
47699
+ * Handle a resync_request from the owner (owner-initiated ratchet re-establishment).
47700
+ * Re-derives shared secret via X3DH as responder, initializes fresh receiver ratchet,
47701
+ * and sends resync_ack back with agent's public keys.
47702
+ */
47703
+ async _handleResyncRequest(data) {
47704
+ const convId = data.conversation_id;
47705
+ console.log(
47706
+ `[SecureChannel] Received resync_request for conv ${convId.slice(0, 8)}... (reason: ${data.reason ?? "unknown"})`
47707
+ );
47708
+ try {
47709
+ if (!this._persisted) {
47710
+ console.error("[SecureChannel] Cannot handle resync \u2014 no persisted state");
47711
+ return;
47712
+ }
47713
+ const identity = this._persisted.identityKeypair;
47714
+ const ephemeral = this._persisted.ephemeralKeypair;
47715
+ const sharedSecret = performX3DH({
47716
+ myIdentityPrivate: hexToBytes(identity.privateKey),
47717
+ myEphemeralPrivate: hexToBytes(ephemeral.privateKey),
47718
+ theirIdentityPublic: hexToBytes(data.identity_public_key),
47719
+ theirEphemeralPublic: hexToBytes(data.ephemeral_public_key),
47720
+ isInitiator: false
47721
+ });
47722
+ const ratchet = DoubleRatchet.initReceiver(sharedSecret, {
47723
+ publicKey: hexToBytes(identity.publicKey),
47724
+ privateKey: hexToBytes(identity.privateKey),
47725
+ keyType: "ed25519"
47726
+ });
47727
+ const existingSession = this._sessions.get(convId);
47728
+ const ownerDeviceId = data.sender_device_id ?? existingSession?.ownerDeviceId ?? "";
47729
+ this._sessions.set(convId, {
47730
+ ownerDeviceId,
47731
+ ratchet,
47732
+ activated: false
47733
+ // Wait for owner's encrypted session_init
47734
+ });
47735
+ this._persisted.sessions[convId] = {
47736
+ ownerDeviceId,
47737
+ ratchetState: ratchet.serialize(),
47738
+ activated: false
47739
+ };
47740
+ await this._persistState();
47741
+ if (this._ws) {
47742
+ this._ws.send(
47743
+ JSON.stringify({
47744
+ event: "resync_ack",
47745
+ data: {
47746
+ conversation_id: convId,
47747
+ identity_public_key: identity.publicKey,
47748
+ ephemeral_public_key: ephemeral.publicKey
47749
+ }
47750
+ })
47751
+ );
47752
+ }
47753
+ console.log(
47754
+ `[SecureChannel] Resync complete for conv ${convId.slice(0, 8)}... \u2014 waiting for owner session_init`
47755
+ );
47756
+ this.emit("resync_completed", { conversationId: convId });
47757
+ } catch (err) {
47758
+ console.error(`[SecureChannel] Resync failed for conv ${convId.slice(0, 8)}...:`, err);
47759
+ this.emit("error", err);
47760
+ }
47761
+ }
47622
47762
  /**
47623
47763
  * Handle an incoming room message. Finds the pairwise conversation
47624
47764
  * for the sender, decrypts, and emits a room_message event.
@@ -47828,8 +47968,28 @@ ${messageText}`;
47828
47968
  const session = this._sessions.get(msg.conversation_id);
47829
47969
  if (!session) {
47830
47970
  console.warn(
47831
- `[SecureChannel] No session for conversation ${msg.conversation_id} during sync, skipping`
47971
+ `[SecureChannel] No session for conversation ${msg.conversation_id} during sync, requesting resync`
47832
47972
  );
47973
+ const RESYNC_COOLDOWN_MS = 5 * 60 * 1e3;
47974
+ const lastResync = this._lastResyncRequest.get(msg.conversation_id) ?? 0;
47975
+ if (Date.now() - lastResync > RESYNC_COOLDOWN_MS && this._ws && this._persisted) {
47976
+ this._lastResyncRequest.set(msg.conversation_id, Date.now());
47977
+ this._ws.send(
47978
+ JSON.stringify({
47979
+ event: "resync_request",
47980
+ data: {
47981
+ conversation_id: msg.conversation_id,
47982
+ reason: "no_session",
47983
+ identity_public_key: this._persisted.identityKeypair.publicKey,
47984
+ ephemeral_public_key: this._persisted.ephemeralKeypair.publicKey
47985
+ }
47986
+ })
47987
+ );
47988
+ this.emit("resync_requested", {
47989
+ conversationId: msg.conversation_id,
47990
+ reason: "no_session"
47991
+ });
47992
+ }
47833
47993
  continue;
47834
47994
  }
47835
47995
  try {