@voidly/agent-sdk 3.2.7 → 3.3.1
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.d.ts +32 -1
- package/dist/index.js +110 -6
- package/dist/index.mjs +110 -6
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -96,6 +96,10 @@ interface VoidlyAgentConfig {
|
|
|
96
96
|
/** Transport preference for listen() — tries in order, falls back automatically
|
|
97
97
|
* Default: ['sse', 'long-poll']. Options: 'websocket' | 'sse' | 'long-poll' */
|
|
98
98
|
transport?: ('websocket' | 'sse' | 'long-poll')[];
|
|
99
|
+
/** Auto-reset ratchet after N consecutive decrypt failures from same peer (default: 10, 0 = disabled) */
|
|
100
|
+
autoResetThreshold?: number;
|
|
101
|
+
/** Callback when a ratchet is auto-reset due to consecutive failures */
|
|
102
|
+
onRatchetReset?: (peerDid: string, failCount: number) => void;
|
|
99
103
|
}
|
|
100
104
|
interface ListenOptions {
|
|
101
105
|
/** Milliseconds between polls (default: 2000, min: 500) */
|
|
@@ -192,6 +196,10 @@ declare class VoidlyAgent {
|
|
|
192
196
|
private _identityCache;
|
|
193
197
|
private _seenMessageIds;
|
|
194
198
|
private _decryptFailCount;
|
|
199
|
+
/** Per-peer consecutive decrypt failure tracking for auto-recovery */
|
|
200
|
+
private _peerDecryptFails;
|
|
201
|
+
/** Max consecutive per-peer decrypt failures before auto-resetting ratchet (0 = disabled) */
|
|
202
|
+
private _autoResetThreshold;
|
|
195
203
|
private _rpcHandlers;
|
|
196
204
|
private _rpcPending;
|
|
197
205
|
private _coverTrafficTimer;
|
|
@@ -202,6 +210,7 @@ declare class VoidlyAgent {
|
|
|
202
210
|
private _persistPath?;
|
|
203
211
|
private _persistKey;
|
|
204
212
|
private _transportPrefs;
|
|
213
|
+
private _onRatchetReset?;
|
|
205
214
|
private constructor();
|
|
206
215
|
/**
|
|
207
216
|
* Register a new agent on the Voidly relay.
|
|
@@ -290,6 +299,24 @@ declare class VoidlyAgent {
|
|
|
290
299
|
* Useful for detecting key mismatches, attacks, or corruption.
|
|
291
300
|
*/
|
|
292
301
|
get decryptFailCount(): number;
|
|
302
|
+
/**
|
|
303
|
+
* Get per-peer consecutive decrypt failure counts.
|
|
304
|
+
* Useful for diagnosing which peer conversations have desynchronized ratchets.
|
|
305
|
+
*/
|
|
306
|
+
get peerDecryptFails(): Record<string, number>;
|
|
307
|
+
/**
|
|
308
|
+
* Reset ratchet state for a specific peer.
|
|
309
|
+
* This clears all ratchet keys for the conversation, causing the next send
|
|
310
|
+
* to re-initialize a fresh DH exchange. The peer's ratchet will also reset
|
|
311
|
+
* automatically when they receive the new-format message.
|
|
312
|
+
*
|
|
313
|
+
* Use this when ratchet desync is detected (e.g., consecutive decrypt failures).
|
|
314
|
+
* After calling this, both sides need to send a message to re-establish the ratchet.
|
|
315
|
+
*
|
|
316
|
+
* @param peerDid - The DID of the peer whose ratchet should be reset
|
|
317
|
+
* @returns true if a ratchet was found and reset, false if no ratchet existed
|
|
318
|
+
*/
|
|
319
|
+
resetRatchet(peerDid: string): boolean;
|
|
293
320
|
/**
|
|
294
321
|
* Generate a did:key identifier from this agent's Ed25519 signing key.
|
|
295
322
|
* did:key is a W3C standard — interoperable across systems.
|
|
@@ -337,7 +364,11 @@ declare class VoidlyAgent {
|
|
|
337
364
|
messageType?: string;
|
|
338
365
|
unreadOnly?: boolean;
|
|
339
366
|
}): Promise<DecryptedMessage[]>;
|
|
340
|
-
/** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports)
|
|
367
|
+
/** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports)
|
|
368
|
+
* Returns decrypted messages AND IDs of messages that failed to decrypt.
|
|
369
|
+
* Failed messages should be marked as read on the relay — they are permanently
|
|
370
|
+
* undecryptable and will poison the queue if left unread (Signal-style handling).
|
|
371
|
+
*/
|
|
341
372
|
private _decryptMessages;
|
|
342
373
|
/**
|
|
343
374
|
* Delete a message by ID (must be sender or recipient).
|
package/dist/index.js
CHANGED
|
@@ -4082,6 +4082,10 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4082
4082
|
this._identityCache = /* @__PURE__ */ new Map();
|
|
4083
4083
|
this._seenMessageIds = /* @__PURE__ */ new Set();
|
|
4084
4084
|
this._decryptFailCount = 0;
|
|
4085
|
+
/** Per-peer consecutive decrypt failure tracking for auto-recovery */
|
|
4086
|
+
this._peerDecryptFails = /* @__PURE__ */ new Map();
|
|
4087
|
+
/** Max consecutive per-peer decrypt failures before auto-resetting ratchet (0 = disabled) */
|
|
4088
|
+
this._autoResetThreshold = 10;
|
|
4085
4089
|
// RPC handlers: method → handler function
|
|
4086
4090
|
this._rpcHandlers = /* @__PURE__ */ new Map();
|
|
4087
4091
|
// RPC pending responses: rpc_id → { resolve, reject, timer }
|
|
@@ -4117,6 +4121,8 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4117
4121
|
this._onLoad = config?.onLoad;
|
|
4118
4122
|
this._persistPath = config?.persistPath;
|
|
4119
4123
|
this._transportPrefs = config?.transport || ["sse", "long-poll"];
|
|
4124
|
+
this._autoResetThreshold = config?.autoResetThreshold ?? 10;
|
|
4125
|
+
this._onRatchetReset = config?.onRatchetReset;
|
|
4120
4126
|
}
|
|
4121
4127
|
// ─── Factory Methods ────────────────────────────────────────────────────────
|
|
4122
4128
|
/**
|
|
@@ -4214,6 +4220,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4214
4220
|
} catch {
|
|
4215
4221
|
}
|
|
4216
4222
|
}
|
|
4223
|
+
const effectiveConfig = !mlkemSk && config?.postQuantum !== false ? { ...config, postQuantum: false } : config;
|
|
4217
4224
|
const agent = new _VoidlyAgent({
|
|
4218
4225
|
did: creds.did,
|
|
4219
4226
|
apiKey: creds.apiKey,
|
|
@@ -4224,7 +4231,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4224
4231
|
},
|
|
4225
4232
|
mlkemPublicKey: mlkemPk,
|
|
4226
4233
|
mlkemSecretKey: mlkemSk
|
|
4227
|
-
},
|
|
4234
|
+
}, effectiveConfig);
|
|
4228
4235
|
if (creds.ratchetStates) {
|
|
4229
4236
|
for (const [pairId, rs] of Object.entries(creds.ratchetStates)) {
|
|
4230
4237
|
try {
|
|
@@ -4519,6 +4526,37 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4519
4526
|
get decryptFailCount() {
|
|
4520
4527
|
return this._decryptFailCount;
|
|
4521
4528
|
}
|
|
4529
|
+
/**
|
|
4530
|
+
* Get per-peer consecutive decrypt failure counts.
|
|
4531
|
+
* Useful for diagnosing which peer conversations have desynchronized ratchets.
|
|
4532
|
+
*/
|
|
4533
|
+
get peerDecryptFails() {
|
|
4534
|
+
return Object.fromEntries(this._peerDecryptFails);
|
|
4535
|
+
}
|
|
4536
|
+
/**
|
|
4537
|
+
* Reset ratchet state for a specific peer.
|
|
4538
|
+
* This clears all ratchet keys for the conversation, causing the next send
|
|
4539
|
+
* to re-initialize a fresh DH exchange. The peer's ratchet will also reset
|
|
4540
|
+
* automatically when they receive the new-format message.
|
|
4541
|
+
*
|
|
4542
|
+
* Use this when ratchet desync is detected (e.g., consecutive decrypt failures).
|
|
4543
|
+
* After calling this, both sides need to send a message to re-establish the ratchet.
|
|
4544
|
+
*
|
|
4545
|
+
* @param peerDid - The DID of the peer whose ratchet should be reset
|
|
4546
|
+
* @returns true if a ratchet was found and reset, false if no ratchet existed
|
|
4547
|
+
*/
|
|
4548
|
+
resetRatchet(peerDid) {
|
|
4549
|
+
const sendPairId = `${this.did}:${peerDid}`;
|
|
4550
|
+
const recvPairId = `${peerDid}:${this.did}`;
|
|
4551
|
+
const hadSend = this._ratchetStates.delete(sendPairId);
|
|
4552
|
+
const hadRecv = this._ratchetStates.delete(recvPairId);
|
|
4553
|
+
this._peerDecryptFails.delete(peerDid);
|
|
4554
|
+
if (hadSend || hadRecv) {
|
|
4555
|
+
this._persistRatchetState().catch(() => {
|
|
4556
|
+
});
|
|
4557
|
+
}
|
|
4558
|
+
return hadSend || hadRecv;
|
|
4559
|
+
}
|
|
4522
4560
|
/**
|
|
4523
4561
|
* Generate a did:key identifier from this agent's Ed25519 signing key.
|
|
4524
4562
|
* did:key is a W3C standard — interoperable across systems.
|
|
@@ -4747,14 +4785,37 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4747
4785
|
throw new Error(`Receive failed: ${err.error?.message || err.error || res.statusText}`);
|
|
4748
4786
|
}
|
|
4749
4787
|
const data = await res.json();
|
|
4750
|
-
|
|
4788
|
+
const { decrypted, failedIds } = await this._decryptMessages(data.messages);
|
|
4789
|
+
if (failedIds.length > 0) {
|
|
4790
|
+
try {
|
|
4791
|
+
await this.markReadBatch(failedIds);
|
|
4792
|
+
} catch {
|
|
4793
|
+
for (const id of failedIds) {
|
|
4794
|
+
try {
|
|
4795
|
+
await this.markRead(id);
|
|
4796
|
+
} catch {
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
return decrypted;
|
|
4751
4802
|
}
|
|
4752
|
-
/** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports)
|
|
4803
|
+
/** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports)
|
|
4804
|
+
* Returns decrypted messages AND IDs of messages that failed to decrypt.
|
|
4805
|
+
* Failed messages should be marked as read on the relay — they are permanently
|
|
4806
|
+
* undecryptable and will poison the queue if left unread (Signal-style handling).
|
|
4807
|
+
*/
|
|
4753
4808
|
async _decryptMessages(rawMessages) {
|
|
4754
4809
|
const decrypted = [];
|
|
4810
|
+
const failedIds = [];
|
|
4811
|
+
const resetPeers = /* @__PURE__ */ new Set();
|
|
4755
4812
|
for (const msg of rawMessages) {
|
|
4756
4813
|
try {
|
|
4757
4814
|
if (this._seenMessageIds.has(msg.id)) continue;
|
|
4815
|
+
if (resetPeers.has(msg.from)) {
|
|
4816
|
+
failedIds.push(msg.id);
|
|
4817
|
+
continue;
|
|
4818
|
+
}
|
|
4758
4819
|
let senderEncPub;
|
|
4759
4820
|
let senderSignPubBytes = null;
|
|
4760
4821
|
if (msg.sender_encryption_key) {
|
|
@@ -4890,6 +4951,15 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4890
4951
|
const skip = targetStep - state.recvStep;
|
|
4891
4952
|
if (skip > MAX_SKIP) {
|
|
4892
4953
|
this._decryptFailCount++;
|
|
4954
|
+
failedIds.push(msg.id);
|
|
4955
|
+
const peerForSkip = msg.from || "unknown";
|
|
4956
|
+
const prevSkipFails = this._peerDecryptFails.get(peerForSkip) || 0;
|
|
4957
|
+
this._peerDecryptFails.set(peerForSkip, prevSkipFails + 1);
|
|
4958
|
+
if (this._autoResetThreshold > 0 && prevSkipFails + 1 >= this._autoResetThreshold) {
|
|
4959
|
+
this.resetRatchet(peerForSkip);
|
|
4960
|
+
resetPeers.add(peerForSkip);
|
|
4961
|
+
this._onRatchetReset?.(peerForSkip, prevSkipFails + 1);
|
|
4962
|
+
}
|
|
4893
4963
|
continue;
|
|
4894
4964
|
} else {
|
|
4895
4965
|
let ck = state.recvChainKey;
|
|
@@ -4917,8 +4987,23 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4917
4987
|
}
|
|
4918
4988
|
if (!rawPlaintext) {
|
|
4919
4989
|
this._decryptFailCount++;
|
|
4990
|
+
failedIds.push(msg.id);
|
|
4991
|
+
const senderForFail = msg.from || "unknown";
|
|
4992
|
+
const prevFails = this._peerDecryptFails.get(senderForFail) || 0;
|
|
4993
|
+
this._peerDecryptFails.set(senderForFail, prevFails + 1);
|
|
4994
|
+
if (this._autoResetThreshold > 0 && prevFails + 1 >= this._autoResetThreshold) {
|
|
4995
|
+
this.resetRatchet(senderForFail);
|
|
4996
|
+
resetPeers.add(senderForFail);
|
|
4997
|
+
this._onRatchetReset?.(senderForFail, prevFails + 1);
|
|
4998
|
+
}
|
|
4920
4999
|
continue;
|
|
4921
5000
|
}
|
|
5001
|
+
{
|
|
5002
|
+
const senderForSuccess = msg.from || "unknown";
|
|
5003
|
+
if (this._peerDecryptFails.has(senderForSuccess)) {
|
|
5004
|
+
this._peerDecryptFails.delete(senderForSuccess);
|
|
5005
|
+
}
|
|
5006
|
+
}
|
|
4922
5007
|
let plaintextBytes = rawPlaintext;
|
|
4923
5008
|
let wasPadded = false;
|
|
4924
5009
|
let wasSealed = false;
|
|
@@ -4983,6 +5068,10 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4983
5068
|
}
|
|
4984
5069
|
if (this.requireSignatures && !signatureValid) {
|
|
4985
5070
|
this._decryptFailCount++;
|
|
5071
|
+
failedIds.push(msg.id);
|
|
5072
|
+
const peerForSig = msg.from || "unknown";
|
|
5073
|
+
const prevSigFails = this._peerDecryptFails.get(peerForSig) || 0;
|
|
5074
|
+
this._peerDecryptFails.set(peerForSig, prevSigFails + 1);
|
|
4986
5075
|
continue;
|
|
4987
5076
|
}
|
|
4988
5077
|
this._seenMessageIds.add(msg.id);
|
|
@@ -5009,13 +5098,22 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
5009
5098
|
});
|
|
5010
5099
|
} catch {
|
|
5011
5100
|
this._decryptFailCount++;
|
|
5101
|
+
failedIds.push(msg.id);
|
|
5102
|
+
const peerForCatch = msg.from || "unknown";
|
|
5103
|
+
const prevCatchFails = this._peerDecryptFails.get(peerForCatch) || 0;
|
|
5104
|
+
this._peerDecryptFails.set(peerForCatch, prevCatchFails + 1);
|
|
5105
|
+
if (this._autoResetThreshold > 0 && prevCatchFails + 1 >= this._autoResetThreshold) {
|
|
5106
|
+
this.resetRatchet(peerForCatch);
|
|
5107
|
+
resetPeers.add(peerForCatch);
|
|
5108
|
+
this._onRatchetReset?.(peerForCatch, prevCatchFails + 1);
|
|
5109
|
+
}
|
|
5012
5110
|
}
|
|
5013
5111
|
}
|
|
5014
|
-
if (decrypted.length > 0) {
|
|
5112
|
+
if (decrypted.length > 0 || this._peerDecryptFails.size > 0) {
|
|
5015
5113
|
this._persistRatchetState().catch(() => {
|
|
5016
5114
|
});
|
|
5017
5115
|
}
|
|
5018
|
-
return decrypted;
|
|
5116
|
+
return { decrypted, failedIds };
|
|
5019
5117
|
}
|
|
5020
5118
|
// ─── Message Management ─────────────────────────────────────────────────────
|
|
5021
5119
|
/**
|
|
@@ -6371,7 +6469,13 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6371
6469
|
if (eventType === "message" && dataStr) {
|
|
6372
6470
|
try {
|
|
6373
6471
|
const rawMsg = JSON.parse(dataStr);
|
|
6374
|
-
const decrypted = await this._decryptMessages([rawMsg]);
|
|
6472
|
+
const { decrypted, failedIds } = await this._decryptMessages([rawMsg]);
|
|
6473
|
+
for (const id of failedIds) {
|
|
6474
|
+
try {
|
|
6475
|
+
await this.markRead(id);
|
|
6476
|
+
} catch {
|
|
6477
|
+
}
|
|
6478
|
+
}
|
|
6375
6479
|
if (decrypted.length > 0) {
|
|
6376
6480
|
consecutiveEmpty = 0;
|
|
6377
6481
|
sseFailures = 0;
|
package/dist/index.mjs
CHANGED
|
@@ -4072,6 +4072,10 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4072
4072
|
this._identityCache = /* @__PURE__ */ new Map();
|
|
4073
4073
|
this._seenMessageIds = /* @__PURE__ */ new Set();
|
|
4074
4074
|
this._decryptFailCount = 0;
|
|
4075
|
+
/** Per-peer consecutive decrypt failure tracking for auto-recovery */
|
|
4076
|
+
this._peerDecryptFails = /* @__PURE__ */ new Map();
|
|
4077
|
+
/** Max consecutive per-peer decrypt failures before auto-resetting ratchet (0 = disabled) */
|
|
4078
|
+
this._autoResetThreshold = 10;
|
|
4075
4079
|
// RPC handlers: method → handler function
|
|
4076
4080
|
this._rpcHandlers = /* @__PURE__ */ new Map();
|
|
4077
4081
|
// RPC pending responses: rpc_id → { resolve, reject, timer }
|
|
@@ -4107,6 +4111,8 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4107
4111
|
this._onLoad = config?.onLoad;
|
|
4108
4112
|
this._persistPath = config?.persistPath;
|
|
4109
4113
|
this._transportPrefs = config?.transport || ["sse", "long-poll"];
|
|
4114
|
+
this._autoResetThreshold = config?.autoResetThreshold ?? 10;
|
|
4115
|
+
this._onRatchetReset = config?.onRatchetReset;
|
|
4110
4116
|
}
|
|
4111
4117
|
// ─── Factory Methods ────────────────────────────────────────────────────────
|
|
4112
4118
|
/**
|
|
@@ -4204,6 +4210,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4204
4210
|
} catch {
|
|
4205
4211
|
}
|
|
4206
4212
|
}
|
|
4213
|
+
const effectiveConfig = !mlkemSk && config?.postQuantum !== false ? { ...config, postQuantum: false } : config;
|
|
4207
4214
|
const agent = new _VoidlyAgent({
|
|
4208
4215
|
did: creds.did,
|
|
4209
4216
|
apiKey: creds.apiKey,
|
|
@@ -4214,7 +4221,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4214
4221
|
},
|
|
4215
4222
|
mlkemPublicKey: mlkemPk,
|
|
4216
4223
|
mlkemSecretKey: mlkemSk
|
|
4217
|
-
},
|
|
4224
|
+
}, effectiveConfig);
|
|
4218
4225
|
if (creds.ratchetStates) {
|
|
4219
4226
|
for (const [pairId, rs] of Object.entries(creds.ratchetStates)) {
|
|
4220
4227
|
try {
|
|
@@ -4509,6 +4516,37 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4509
4516
|
get decryptFailCount() {
|
|
4510
4517
|
return this._decryptFailCount;
|
|
4511
4518
|
}
|
|
4519
|
+
/**
|
|
4520
|
+
* Get per-peer consecutive decrypt failure counts.
|
|
4521
|
+
* Useful for diagnosing which peer conversations have desynchronized ratchets.
|
|
4522
|
+
*/
|
|
4523
|
+
get peerDecryptFails() {
|
|
4524
|
+
return Object.fromEntries(this._peerDecryptFails);
|
|
4525
|
+
}
|
|
4526
|
+
/**
|
|
4527
|
+
* Reset ratchet state for a specific peer.
|
|
4528
|
+
* This clears all ratchet keys for the conversation, causing the next send
|
|
4529
|
+
* to re-initialize a fresh DH exchange. The peer's ratchet will also reset
|
|
4530
|
+
* automatically when they receive the new-format message.
|
|
4531
|
+
*
|
|
4532
|
+
* Use this when ratchet desync is detected (e.g., consecutive decrypt failures).
|
|
4533
|
+
* After calling this, both sides need to send a message to re-establish the ratchet.
|
|
4534
|
+
*
|
|
4535
|
+
* @param peerDid - The DID of the peer whose ratchet should be reset
|
|
4536
|
+
* @returns true if a ratchet was found and reset, false if no ratchet existed
|
|
4537
|
+
*/
|
|
4538
|
+
resetRatchet(peerDid) {
|
|
4539
|
+
const sendPairId = `${this.did}:${peerDid}`;
|
|
4540
|
+
const recvPairId = `${peerDid}:${this.did}`;
|
|
4541
|
+
const hadSend = this._ratchetStates.delete(sendPairId);
|
|
4542
|
+
const hadRecv = this._ratchetStates.delete(recvPairId);
|
|
4543
|
+
this._peerDecryptFails.delete(peerDid);
|
|
4544
|
+
if (hadSend || hadRecv) {
|
|
4545
|
+
this._persistRatchetState().catch(() => {
|
|
4546
|
+
});
|
|
4547
|
+
}
|
|
4548
|
+
return hadSend || hadRecv;
|
|
4549
|
+
}
|
|
4512
4550
|
/**
|
|
4513
4551
|
* Generate a did:key identifier from this agent's Ed25519 signing key.
|
|
4514
4552
|
* did:key is a W3C standard — interoperable across systems.
|
|
@@ -4737,14 +4775,37 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4737
4775
|
throw new Error(`Receive failed: ${err.error?.message || err.error || res.statusText}`);
|
|
4738
4776
|
}
|
|
4739
4777
|
const data = await res.json();
|
|
4740
|
-
|
|
4778
|
+
const { decrypted, failedIds } = await this._decryptMessages(data.messages);
|
|
4779
|
+
if (failedIds.length > 0) {
|
|
4780
|
+
try {
|
|
4781
|
+
await this.markReadBatch(failedIds);
|
|
4782
|
+
} catch {
|
|
4783
|
+
for (const id of failedIds) {
|
|
4784
|
+
try {
|
|
4785
|
+
await this.markRead(id);
|
|
4786
|
+
} catch {
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
return decrypted;
|
|
4741
4792
|
}
|
|
4742
|
-
/** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports)
|
|
4793
|
+
/** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports)
|
|
4794
|
+
* Returns decrypted messages AND IDs of messages that failed to decrypt.
|
|
4795
|
+
* Failed messages should be marked as read on the relay — they are permanently
|
|
4796
|
+
* undecryptable and will poison the queue if left unread (Signal-style handling).
|
|
4797
|
+
*/
|
|
4743
4798
|
async _decryptMessages(rawMessages) {
|
|
4744
4799
|
const decrypted = [];
|
|
4800
|
+
const failedIds = [];
|
|
4801
|
+
const resetPeers = /* @__PURE__ */ new Set();
|
|
4745
4802
|
for (const msg of rawMessages) {
|
|
4746
4803
|
try {
|
|
4747
4804
|
if (this._seenMessageIds.has(msg.id)) continue;
|
|
4805
|
+
if (resetPeers.has(msg.from)) {
|
|
4806
|
+
failedIds.push(msg.id);
|
|
4807
|
+
continue;
|
|
4808
|
+
}
|
|
4748
4809
|
let senderEncPub;
|
|
4749
4810
|
let senderSignPubBytes = null;
|
|
4750
4811
|
if (msg.sender_encryption_key) {
|
|
@@ -4880,6 +4941,15 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4880
4941
|
const skip = targetStep - state.recvStep;
|
|
4881
4942
|
if (skip > MAX_SKIP) {
|
|
4882
4943
|
this._decryptFailCount++;
|
|
4944
|
+
failedIds.push(msg.id);
|
|
4945
|
+
const peerForSkip = msg.from || "unknown";
|
|
4946
|
+
const prevSkipFails = this._peerDecryptFails.get(peerForSkip) || 0;
|
|
4947
|
+
this._peerDecryptFails.set(peerForSkip, prevSkipFails + 1);
|
|
4948
|
+
if (this._autoResetThreshold > 0 && prevSkipFails + 1 >= this._autoResetThreshold) {
|
|
4949
|
+
this.resetRatchet(peerForSkip);
|
|
4950
|
+
resetPeers.add(peerForSkip);
|
|
4951
|
+
this._onRatchetReset?.(peerForSkip, prevSkipFails + 1);
|
|
4952
|
+
}
|
|
4883
4953
|
continue;
|
|
4884
4954
|
} else {
|
|
4885
4955
|
let ck = state.recvChainKey;
|
|
@@ -4907,8 +4977,23 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4907
4977
|
}
|
|
4908
4978
|
if (!rawPlaintext) {
|
|
4909
4979
|
this._decryptFailCount++;
|
|
4980
|
+
failedIds.push(msg.id);
|
|
4981
|
+
const senderForFail = msg.from || "unknown";
|
|
4982
|
+
const prevFails = this._peerDecryptFails.get(senderForFail) || 0;
|
|
4983
|
+
this._peerDecryptFails.set(senderForFail, prevFails + 1);
|
|
4984
|
+
if (this._autoResetThreshold > 0 && prevFails + 1 >= this._autoResetThreshold) {
|
|
4985
|
+
this.resetRatchet(senderForFail);
|
|
4986
|
+
resetPeers.add(senderForFail);
|
|
4987
|
+
this._onRatchetReset?.(senderForFail, prevFails + 1);
|
|
4988
|
+
}
|
|
4910
4989
|
continue;
|
|
4911
4990
|
}
|
|
4991
|
+
{
|
|
4992
|
+
const senderForSuccess = msg.from || "unknown";
|
|
4993
|
+
if (this._peerDecryptFails.has(senderForSuccess)) {
|
|
4994
|
+
this._peerDecryptFails.delete(senderForSuccess);
|
|
4995
|
+
}
|
|
4996
|
+
}
|
|
4912
4997
|
let plaintextBytes = rawPlaintext;
|
|
4913
4998
|
let wasPadded = false;
|
|
4914
4999
|
let wasSealed = false;
|
|
@@ -4973,6 +5058,10 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4973
5058
|
}
|
|
4974
5059
|
if (this.requireSignatures && !signatureValid) {
|
|
4975
5060
|
this._decryptFailCount++;
|
|
5061
|
+
failedIds.push(msg.id);
|
|
5062
|
+
const peerForSig = msg.from || "unknown";
|
|
5063
|
+
const prevSigFails = this._peerDecryptFails.get(peerForSig) || 0;
|
|
5064
|
+
this._peerDecryptFails.set(peerForSig, prevSigFails + 1);
|
|
4976
5065
|
continue;
|
|
4977
5066
|
}
|
|
4978
5067
|
this._seenMessageIds.add(msg.id);
|
|
@@ -4999,13 +5088,22 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4999
5088
|
});
|
|
5000
5089
|
} catch {
|
|
5001
5090
|
this._decryptFailCount++;
|
|
5091
|
+
failedIds.push(msg.id);
|
|
5092
|
+
const peerForCatch = msg.from || "unknown";
|
|
5093
|
+
const prevCatchFails = this._peerDecryptFails.get(peerForCatch) || 0;
|
|
5094
|
+
this._peerDecryptFails.set(peerForCatch, prevCatchFails + 1);
|
|
5095
|
+
if (this._autoResetThreshold > 0 && prevCatchFails + 1 >= this._autoResetThreshold) {
|
|
5096
|
+
this.resetRatchet(peerForCatch);
|
|
5097
|
+
resetPeers.add(peerForCatch);
|
|
5098
|
+
this._onRatchetReset?.(peerForCatch, prevCatchFails + 1);
|
|
5099
|
+
}
|
|
5002
5100
|
}
|
|
5003
5101
|
}
|
|
5004
|
-
if (decrypted.length > 0) {
|
|
5102
|
+
if (decrypted.length > 0 || this._peerDecryptFails.size > 0) {
|
|
5005
5103
|
this._persistRatchetState().catch(() => {
|
|
5006
5104
|
});
|
|
5007
5105
|
}
|
|
5008
|
-
return decrypted;
|
|
5106
|
+
return { decrypted, failedIds };
|
|
5009
5107
|
}
|
|
5010
5108
|
// ─── Message Management ─────────────────────────────────────────────────────
|
|
5011
5109
|
/**
|
|
@@ -6361,7 +6459,13 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6361
6459
|
if (eventType === "message" && dataStr) {
|
|
6362
6460
|
try {
|
|
6363
6461
|
const rawMsg = JSON.parse(dataStr);
|
|
6364
|
-
const decrypted = await this._decryptMessages([rawMsg]);
|
|
6462
|
+
const { decrypted, failedIds } = await this._decryptMessages([rawMsg]);
|
|
6463
|
+
for (const id of failedIds) {
|
|
6464
|
+
try {
|
|
6465
|
+
await this.markRead(id);
|
|
6466
|
+
} catch {
|
|
6467
|
+
}
|
|
6468
|
+
}
|
|
6365
6469
|
if (decrypted.length > 0) {
|
|
6366
6470
|
consecutiveEmpty = 0;
|
|
6367
6471
|
sseFailures = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidly/agent-sdk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "E2E encrypted agent-to-agent communication SDK — Double Ratchet, X3DH, deniable auth, ML-KEM-768 post-quantum, SSE streaming, ratchet persistence, multi-relay federation",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|