@voidly/agent-sdk 3.2.6 → 3.3.0
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 +27 -0
- package/dist/index.js +73 -1
- package/dist/index.mjs +73 -1
- package/examples/post-quantum.mjs +99 -0
- package/examples/sse-streaming.mjs +52 -0
- 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.
|
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
|
/**
|
|
@@ -4519,6 +4525,37 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4519
4525
|
get decryptFailCount() {
|
|
4520
4526
|
return this._decryptFailCount;
|
|
4521
4527
|
}
|
|
4528
|
+
/**
|
|
4529
|
+
* Get per-peer consecutive decrypt failure counts.
|
|
4530
|
+
* Useful for diagnosing which peer conversations have desynchronized ratchets.
|
|
4531
|
+
*/
|
|
4532
|
+
get peerDecryptFails() {
|
|
4533
|
+
return Object.fromEntries(this._peerDecryptFails);
|
|
4534
|
+
}
|
|
4535
|
+
/**
|
|
4536
|
+
* Reset ratchet state for a specific peer.
|
|
4537
|
+
* This clears all ratchet keys for the conversation, causing the next send
|
|
4538
|
+
* to re-initialize a fresh DH exchange. The peer's ratchet will also reset
|
|
4539
|
+
* automatically when they receive the new-format message.
|
|
4540
|
+
*
|
|
4541
|
+
* Use this when ratchet desync is detected (e.g., consecutive decrypt failures).
|
|
4542
|
+
* After calling this, both sides need to send a message to re-establish the ratchet.
|
|
4543
|
+
*
|
|
4544
|
+
* @param peerDid - The DID of the peer whose ratchet should be reset
|
|
4545
|
+
* @returns true if a ratchet was found and reset, false if no ratchet existed
|
|
4546
|
+
*/
|
|
4547
|
+
resetRatchet(peerDid) {
|
|
4548
|
+
const sendPairId = `${this.did}:${peerDid}`;
|
|
4549
|
+
const recvPairId = `${peerDid}:${this.did}`;
|
|
4550
|
+
const hadSend = this._ratchetStates.delete(sendPairId);
|
|
4551
|
+
const hadRecv = this._ratchetStates.delete(recvPairId);
|
|
4552
|
+
this._peerDecryptFails.delete(peerDid);
|
|
4553
|
+
if (hadSend || hadRecv) {
|
|
4554
|
+
this._persistRatchetState().catch(() => {
|
|
4555
|
+
});
|
|
4556
|
+
}
|
|
4557
|
+
return hadSend || hadRecv;
|
|
4558
|
+
}
|
|
4522
4559
|
/**
|
|
4523
4560
|
* Generate a did:key identifier from this agent's Ed25519 signing key.
|
|
4524
4561
|
* did:key is a W3C standard — interoperable across systems.
|
|
@@ -4752,9 +4789,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4752
4789
|
/** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports) */
|
|
4753
4790
|
async _decryptMessages(rawMessages) {
|
|
4754
4791
|
const decrypted = [];
|
|
4792
|
+
const resetPeers = /* @__PURE__ */ new Set();
|
|
4755
4793
|
for (const msg of rawMessages) {
|
|
4756
4794
|
try {
|
|
4757
4795
|
if (this._seenMessageIds.has(msg.id)) continue;
|
|
4796
|
+
if (resetPeers.has(msg.from)) continue;
|
|
4758
4797
|
let senderEncPub;
|
|
4759
4798
|
let senderSignPubBytes = null;
|
|
4760
4799
|
if (msg.sender_encryption_key) {
|
|
@@ -4890,6 +4929,14 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4890
4929
|
const skip = targetStep - state.recvStep;
|
|
4891
4930
|
if (skip > MAX_SKIP) {
|
|
4892
4931
|
this._decryptFailCount++;
|
|
4932
|
+
const peerForSkip = msg.from || "unknown";
|
|
4933
|
+
const prevSkipFails = this._peerDecryptFails.get(peerForSkip) || 0;
|
|
4934
|
+
this._peerDecryptFails.set(peerForSkip, prevSkipFails + 1);
|
|
4935
|
+
if (this._autoResetThreshold > 0 && prevSkipFails + 1 >= this._autoResetThreshold) {
|
|
4936
|
+
this.resetRatchet(peerForSkip);
|
|
4937
|
+
resetPeers.add(peerForSkip);
|
|
4938
|
+
this._onRatchetReset?.(peerForSkip, prevSkipFails + 1);
|
|
4939
|
+
}
|
|
4893
4940
|
continue;
|
|
4894
4941
|
} else {
|
|
4895
4942
|
let ck = state.recvChainKey;
|
|
@@ -4917,8 +4964,22 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4917
4964
|
}
|
|
4918
4965
|
if (!rawPlaintext) {
|
|
4919
4966
|
this._decryptFailCount++;
|
|
4967
|
+
const senderForFail = msg.from || "unknown";
|
|
4968
|
+
const prevFails = this._peerDecryptFails.get(senderForFail) || 0;
|
|
4969
|
+
this._peerDecryptFails.set(senderForFail, prevFails + 1);
|
|
4970
|
+
if (this._autoResetThreshold > 0 && prevFails + 1 >= this._autoResetThreshold) {
|
|
4971
|
+
this.resetRatchet(senderForFail);
|
|
4972
|
+
resetPeers.add(senderForFail);
|
|
4973
|
+
this._onRatchetReset?.(senderForFail, prevFails + 1);
|
|
4974
|
+
}
|
|
4920
4975
|
continue;
|
|
4921
4976
|
}
|
|
4977
|
+
{
|
|
4978
|
+
const senderForSuccess = msg.from || "unknown";
|
|
4979
|
+
if (this._peerDecryptFails.has(senderForSuccess)) {
|
|
4980
|
+
this._peerDecryptFails.delete(senderForSuccess);
|
|
4981
|
+
}
|
|
4982
|
+
}
|
|
4922
4983
|
let plaintextBytes = rawPlaintext;
|
|
4923
4984
|
let wasPadded = false;
|
|
4924
4985
|
let wasSealed = false;
|
|
@@ -4983,6 +5044,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4983
5044
|
}
|
|
4984
5045
|
if (this.requireSignatures && !signatureValid) {
|
|
4985
5046
|
this._decryptFailCount++;
|
|
5047
|
+
const peerForSig = msg.from || "unknown";
|
|
5048
|
+
const prevSigFails = this._peerDecryptFails.get(peerForSig) || 0;
|
|
5049
|
+
this._peerDecryptFails.set(peerForSig, prevSigFails + 1);
|
|
4986
5050
|
continue;
|
|
4987
5051
|
}
|
|
4988
5052
|
this._seenMessageIds.add(msg.id);
|
|
@@ -5009,9 +5073,17 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
5009
5073
|
});
|
|
5010
5074
|
} catch {
|
|
5011
5075
|
this._decryptFailCount++;
|
|
5076
|
+
const peerForCatch = msg.from || "unknown";
|
|
5077
|
+
const prevCatchFails = this._peerDecryptFails.get(peerForCatch) || 0;
|
|
5078
|
+
this._peerDecryptFails.set(peerForCatch, prevCatchFails + 1);
|
|
5079
|
+
if (this._autoResetThreshold > 0 && prevCatchFails + 1 >= this._autoResetThreshold) {
|
|
5080
|
+
this.resetRatchet(peerForCatch);
|
|
5081
|
+
resetPeers.add(peerForCatch);
|
|
5082
|
+
this._onRatchetReset?.(peerForCatch, prevCatchFails + 1);
|
|
5083
|
+
}
|
|
5012
5084
|
}
|
|
5013
5085
|
}
|
|
5014
|
-
if (decrypted.length > 0) {
|
|
5086
|
+
if (decrypted.length > 0 || this._peerDecryptFails.size > 0) {
|
|
5015
5087
|
this._persistRatchetState().catch(() => {
|
|
5016
5088
|
});
|
|
5017
5089
|
}
|
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
|
/**
|
|
@@ -4509,6 +4515,37 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4509
4515
|
get decryptFailCount() {
|
|
4510
4516
|
return this._decryptFailCount;
|
|
4511
4517
|
}
|
|
4518
|
+
/**
|
|
4519
|
+
* Get per-peer consecutive decrypt failure counts.
|
|
4520
|
+
* Useful for diagnosing which peer conversations have desynchronized ratchets.
|
|
4521
|
+
*/
|
|
4522
|
+
get peerDecryptFails() {
|
|
4523
|
+
return Object.fromEntries(this._peerDecryptFails);
|
|
4524
|
+
}
|
|
4525
|
+
/**
|
|
4526
|
+
* Reset ratchet state for a specific peer.
|
|
4527
|
+
* This clears all ratchet keys for the conversation, causing the next send
|
|
4528
|
+
* to re-initialize a fresh DH exchange. The peer's ratchet will also reset
|
|
4529
|
+
* automatically when they receive the new-format message.
|
|
4530
|
+
*
|
|
4531
|
+
* Use this when ratchet desync is detected (e.g., consecutive decrypt failures).
|
|
4532
|
+
* After calling this, both sides need to send a message to re-establish the ratchet.
|
|
4533
|
+
*
|
|
4534
|
+
* @param peerDid - The DID of the peer whose ratchet should be reset
|
|
4535
|
+
* @returns true if a ratchet was found and reset, false if no ratchet existed
|
|
4536
|
+
*/
|
|
4537
|
+
resetRatchet(peerDid) {
|
|
4538
|
+
const sendPairId = `${this.did}:${peerDid}`;
|
|
4539
|
+
const recvPairId = `${peerDid}:${this.did}`;
|
|
4540
|
+
const hadSend = this._ratchetStates.delete(sendPairId);
|
|
4541
|
+
const hadRecv = this._ratchetStates.delete(recvPairId);
|
|
4542
|
+
this._peerDecryptFails.delete(peerDid);
|
|
4543
|
+
if (hadSend || hadRecv) {
|
|
4544
|
+
this._persistRatchetState().catch(() => {
|
|
4545
|
+
});
|
|
4546
|
+
}
|
|
4547
|
+
return hadSend || hadRecv;
|
|
4548
|
+
}
|
|
4512
4549
|
/**
|
|
4513
4550
|
* Generate a did:key identifier from this agent's Ed25519 signing key.
|
|
4514
4551
|
* did:key is a W3C standard — interoperable across systems.
|
|
@@ -4742,9 +4779,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4742
4779
|
/** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports) */
|
|
4743
4780
|
async _decryptMessages(rawMessages) {
|
|
4744
4781
|
const decrypted = [];
|
|
4782
|
+
const resetPeers = /* @__PURE__ */ new Set();
|
|
4745
4783
|
for (const msg of rawMessages) {
|
|
4746
4784
|
try {
|
|
4747
4785
|
if (this._seenMessageIds.has(msg.id)) continue;
|
|
4786
|
+
if (resetPeers.has(msg.from)) continue;
|
|
4748
4787
|
let senderEncPub;
|
|
4749
4788
|
let senderSignPubBytes = null;
|
|
4750
4789
|
if (msg.sender_encryption_key) {
|
|
@@ -4880,6 +4919,14 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4880
4919
|
const skip = targetStep - state.recvStep;
|
|
4881
4920
|
if (skip > MAX_SKIP) {
|
|
4882
4921
|
this._decryptFailCount++;
|
|
4922
|
+
const peerForSkip = msg.from || "unknown";
|
|
4923
|
+
const prevSkipFails = this._peerDecryptFails.get(peerForSkip) || 0;
|
|
4924
|
+
this._peerDecryptFails.set(peerForSkip, prevSkipFails + 1);
|
|
4925
|
+
if (this._autoResetThreshold > 0 && prevSkipFails + 1 >= this._autoResetThreshold) {
|
|
4926
|
+
this.resetRatchet(peerForSkip);
|
|
4927
|
+
resetPeers.add(peerForSkip);
|
|
4928
|
+
this._onRatchetReset?.(peerForSkip, prevSkipFails + 1);
|
|
4929
|
+
}
|
|
4883
4930
|
continue;
|
|
4884
4931
|
} else {
|
|
4885
4932
|
let ck = state.recvChainKey;
|
|
@@ -4907,8 +4954,22 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4907
4954
|
}
|
|
4908
4955
|
if (!rawPlaintext) {
|
|
4909
4956
|
this._decryptFailCount++;
|
|
4957
|
+
const senderForFail = msg.from || "unknown";
|
|
4958
|
+
const prevFails = this._peerDecryptFails.get(senderForFail) || 0;
|
|
4959
|
+
this._peerDecryptFails.set(senderForFail, prevFails + 1);
|
|
4960
|
+
if (this._autoResetThreshold > 0 && prevFails + 1 >= this._autoResetThreshold) {
|
|
4961
|
+
this.resetRatchet(senderForFail);
|
|
4962
|
+
resetPeers.add(senderForFail);
|
|
4963
|
+
this._onRatchetReset?.(senderForFail, prevFails + 1);
|
|
4964
|
+
}
|
|
4910
4965
|
continue;
|
|
4911
4966
|
}
|
|
4967
|
+
{
|
|
4968
|
+
const senderForSuccess = msg.from || "unknown";
|
|
4969
|
+
if (this._peerDecryptFails.has(senderForSuccess)) {
|
|
4970
|
+
this._peerDecryptFails.delete(senderForSuccess);
|
|
4971
|
+
}
|
|
4972
|
+
}
|
|
4912
4973
|
let plaintextBytes = rawPlaintext;
|
|
4913
4974
|
let wasPadded = false;
|
|
4914
4975
|
let wasSealed = false;
|
|
@@ -4973,6 +5034,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4973
5034
|
}
|
|
4974
5035
|
if (this.requireSignatures && !signatureValid) {
|
|
4975
5036
|
this._decryptFailCount++;
|
|
5037
|
+
const peerForSig = msg.from || "unknown";
|
|
5038
|
+
const prevSigFails = this._peerDecryptFails.get(peerForSig) || 0;
|
|
5039
|
+
this._peerDecryptFails.set(peerForSig, prevSigFails + 1);
|
|
4976
5040
|
continue;
|
|
4977
5041
|
}
|
|
4978
5042
|
this._seenMessageIds.add(msg.id);
|
|
@@ -4999,9 +5063,17 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4999
5063
|
});
|
|
5000
5064
|
} catch {
|
|
5001
5065
|
this._decryptFailCount++;
|
|
5066
|
+
const peerForCatch = msg.from || "unknown";
|
|
5067
|
+
const prevCatchFails = this._peerDecryptFails.get(peerForCatch) || 0;
|
|
5068
|
+
this._peerDecryptFails.set(peerForCatch, prevCatchFails + 1);
|
|
5069
|
+
if (this._autoResetThreshold > 0 && prevCatchFails + 1 >= this._autoResetThreshold) {
|
|
5070
|
+
this.resetRatchet(peerForCatch);
|
|
5071
|
+
resetPeers.add(peerForCatch);
|
|
5072
|
+
this._onRatchetReset?.(peerForCatch, prevCatchFails + 1);
|
|
5073
|
+
}
|
|
5002
5074
|
}
|
|
5003
5075
|
}
|
|
5004
|
-
if (decrypted.length > 0) {
|
|
5076
|
+
if (decrypted.length > 0 || this._peerDecryptFails.size > 0) {
|
|
5005
5077
|
this._persistRatchetState().catch(() => {
|
|
5006
5078
|
});
|
|
5007
5079
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Post-Quantum + Ratchet Persistence — Future-proof encrypted messaging.
|
|
4
|
+
*
|
|
5
|
+
* Run: node examples/post-quantum.mjs
|
|
6
|
+
*
|
|
7
|
+
* Features demonstrated:
|
|
8
|
+
* 1. ML-KEM-768 hybrid key exchange (NIST FIPS 203) — quantum-resistant
|
|
9
|
+
* 2. Double Ratchet forward secrecy — compromise now can't decrypt past messages
|
|
10
|
+
* 3. Ratchet persistence — session survives agent restarts
|
|
11
|
+
* 4. Sealed sender — relay can't see who sent the message
|
|
12
|
+
* 5. Deniable auth — both parties can produce the signature (plausible deniability)
|
|
13
|
+
*/
|
|
14
|
+
import { VoidlyAgent } from '@voidly/agent-sdk';
|
|
15
|
+
|
|
16
|
+
// ─── Register with full security config ─────────────────────────────────────
|
|
17
|
+
const alice = await VoidlyAgent.register(
|
|
18
|
+
{ name: 'pq-alice', capabilities: ['chat', 'intel'] },
|
|
19
|
+
{
|
|
20
|
+
pq: true, // Enable ML-KEM-768 post-quantum hybrid
|
|
21
|
+
padding: true, // Pad all messages to power-of-2 (traffic analysis resistance)
|
|
22
|
+
sealedSender: true, // Hide sender DID from relay metadata
|
|
23
|
+
deniable: true, // HMAC signatures (both parties can produce — deniable)
|
|
24
|
+
persist: 'memory', // Ratchet state persists in memory (use 'file' or 'relay' for disk)
|
|
25
|
+
autoPin: true, // TOFU: pin first-seen keys, warn on change
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const bob = await VoidlyAgent.register(
|
|
30
|
+
{ name: 'pq-bob', capabilities: ['chat', 'analysis'] },
|
|
31
|
+
{
|
|
32
|
+
pq: true,
|
|
33
|
+
padding: true,
|
|
34
|
+
sealedSender: true,
|
|
35
|
+
deniable: true,
|
|
36
|
+
persist: 'memory',
|
|
37
|
+
autoPin: true,
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
console.log(`Alice: ${alice.did} (PQ + sealed + deniable)`);
|
|
42
|
+
console.log(`Bob: ${bob.did} (PQ + sealed + deniable)\n`);
|
|
43
|
+
|
|
44
|
+
// ─── Verify post-quantum is active ──────────────────────────────────────────
|
|
45
|
+
const threat = alice.threatModel();
|
|
46
|
+
console.log('Active protections:');
|
|
47
|
+
threat.protections.forEach(p => console.log(` ✓ ${p}`));
|
|
48
|
+
console.log('Known gaps:');
|
|
49
|
+
threat.gaps.forEach(g => console.log(` ⚠ ${g}`));
|
|
50
|
+
|
|
51
|
+
// ─── Send messages with full protection stack ───────────────────────────────
|
|
52
|
+
console.log('\nSending with PQ hybrid + Double Ratchet + sealed sender + padding...');
|
|
53
|
+
await alice.send(bob.did, 'Quantum-resistant hello from Alice', { threadId: 'pq-demo' });
|
|
54
|
+
await alice.send(bob.did, 'Even a quantum computer cannot decrypt this retroactively');
|
|
55
|
+
|
|
56
|
+
// Bob receives and decrypts
|
|
57
|
+
const messages = await bob.receive({ limit: 10 });
|
|
58
|
+
for (const msg of messages) {
|
|
59
|
+
console.log(`\n Bob received: "${msg.content}"`);
|
|
60
|
+
console.log(` From: ${msg.from.slice(0, 30)}...`);
|
|
61
|
+
console.log(` Signature valid: ${msg.signatureValid}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Demonstrate forward secrecy ────────────────────────────────────────────
|
|
65
|
+
console.log('\n─── Forward Secrecy Demo ───');
|
|
66
|
+
console.log('Ratchet advances with each message — past keys are deleted.');
|
|
67
|
+
console.log('Even if an attacker compromises the agent NOW, they cannot');
|
|
68
|
+
console.log('decrypt messages that were already received and processed.\n');
|
|
69
|
+
|
|
70
|
+
// Bob replies — DH ratchet advances
|
|
71
|
+
await bob.send(alice.did, 'Reply from Bob — ratchet advanced');
|
|
72
|
+
const replies = await alice.receive({ limit: 5 });
|
|
73
|
+
for (const r of replies) {
|
|
74
|
+
console.log(` Alice received: "${r.content}"`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Credential export (includes ratchet state) ─────────────────────────────
|
|
78
|
+
console.log('\n─── Credential Export ───');
|
|
79
|
+
const creds = alice.exportCredentials();
|
|
80
|
+
console.log(` DID: ${creds.did}`);
|
|
81
|
+
console.log(` Signing key: ${creds.signingSecretKey.slice(0, 16)}...`);
|
|
82
|
+
console.log(` PQ enabled: ${!!creds.mlkemSecretKey}`);
|
|
83
|
+
|
|
84
|
+
// Restore agent from credentials (e.g., after restart)
|
|
85
|
+
const restored = VoidlyAgent.fromCredentials(creds, {
|
|
86
|
+
pq: true, padding: true, sealedSender: true, deniable: true, persist: 'memory',
|
|
87
|
+
});
|
|
88
|
+
console.log(` Restored DID: ${restored.did} (matches: ${restored.did === alice.did})`);
|
|
89
|
+
|
|
90
|
+
// ─── Flush ratchet state ────────────────────────────────────────────────────
|
|
91
|
+
await alice.flushRatchetState();
|
|
92
|
+
console.log('\n Ratchet state flushed to persistence backend.');
|
|
93
|
+
|
|
94
|
+
// Clean shutdown
|
|
95
|
+
alice.stopAll();
|
|
96
|
+
bob.stopAll();
|
|
97
|
+
restored.stopAll();
|
|
98
|
+
|
|
99
|
+
console.log('\n✓ Done — post-quantum hybrid encryption with forward secrecy.');
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SSE Streaming — Real-time message delivery via Server-Sent Events.
|
|
4
|
+
*
|
|
5
|
+
* Run: node examples/sse-streaming.mjs
|
|
6
|
+
*
|
|
7
|
+
* Instead of polling, Bob opens an SSE connection to the relay.
|
|
8
|
+
* Messages arrive in near-real-time (~1s latency) with automatic reconnection.
|
|
9
|
+
* All decryption still happens client-side.
|
|
10
|
+
*/
|
|
11
|
+
import { VoidlyAgent } from '@voidly/agent-sdk';
|
|
12
|
+
|
|
13
|
+
const alice = await VoidlyAgent.register({ name: 'sse-alice' });
|
|
14
|
+
const bob = await VoidlyAgent.register(
|
|
15
|
+
{ name: 'sse-bob' },
|
|
16
|
+
{ transport: ['sse', 'long-poll'] } // Prefer SSE, fall back to long-poll
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
console.log(`Alice: ${alice.did}`);
|
|
20
|
+
console.log(`Bob: ${bob.did} (SSE transport)\n`);
|
|
21
|
+
|
|
22
|
+
// Bob listens via SSE — messages arrive in near-real-time
|
|
23
|
+
const handle = bob.listen(
|
|
24
|
+
(msg) => {
|
|
25
|
+
console.log(` ← Bob received: "${msg.content}" from ${msg.from.slice(0, 24)}...`);
|
|
26
|
+
console.log(` Signature valid: ${msg.signatureValid}`);
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
interval: 2000, // Reconnect interval if SSE drops
|
|
30
|
+
adaptive: true, // Back off when idle
|
|
31
|
+
heartbeat: false, // Don't send pings
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Wait for SSE connection to establish
|
|
36
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
37
|
+
|
|
38
|
+
// Alice sends a burst of messages
|
|
39
|
+
console.log('Alice sending 3 messages...');
|
|
40
|
+
await alice.send(bob.did, 'Message 1 — SSE delivery');
|
|
41
|
+
await alice.send(bob.did, 'Message 2 — near-real-time');
|
|
42
|
+
await alice.send(bob.did, 'Message 3 — all encrypted');
|
|
43
|
+
|
|
44
|
+
// Wait for delivery
|
|
45
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
46
|
+
|
|
47
|
+
// Clean shutdown
|
|
48
|
+
handle.stop();
|
|
49
|
+
alice.stopAll();
|
|
50
|
+
bob.stopAll();
|
|
51
|
+
|
|
52
|
+
console.log('\n✓ Done — SSE streaming with E2E encryption.');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidly/agent-sdk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
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",
|