@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/channel.d.ts +8 -0
- package/dist/channel.d.ts.map +1 -1
- package/dist/cli.js +189 -29
- package/dist/cli.js.map +2 -2
- package/dist/index.js +189 -29
- package/dist/index.js.map +2 -2
- package/package.json +9 -1
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
|
-
|
|
44911
|
-
|
|
44912
|
-
|
|
44913
|
-
|
|
44914
|
-
|
|
44915
|
-
|
|
44916
|
-
|
|
44917
|
-
|
|
44918
|
-
|
|
44919
|
-
|
|
44920
|
-
|
|
44921
|
-
|
|
44922
|
-
|
|
44923
|
-
|
|
44924
|
-
|
|
44925
|
-
|
|
44926
|
-
|
|
44927
|
-
|
|
44928
|
-
|
|
44929
|
-
|
|
44930
|
-
|
|
44931
|
-
|
|
44932
|
-
|
|
44933
|
-
|
|
44934
|
-
|
|
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},
|
|
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,
|
|
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 {
|