@voidly/agent-sdk 3.1.0 → 3.2.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.mts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +339 -66
- package/dist/index.mjs +339 -66
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -85,6 +85,17 @@ interface VoidlyAgentConfig {
|
|
|
85
85
|
jitterMs?: number;
|
|
86
86
|
/** Use long-poll for listen() instead of short-interval polling (default: true) */
|
|
87
87
|
longPoll?: boolean;
|
|
88
|
+
/** Auto-persist ratchet state after every send/receive (default: 'memory' = no persistence) */
|
|
89
|
+
persist?: 'memory' | 'localStorage' | 'indexedDB' | 'file' | 'relay' | 'custom';
|
|
90
|
+
/** Custom persistence: called after each ratchet step with encrypted blob */
|
|
91
|
+
onPersist?: (encryptedState: string) => void | Promise<void>;
|
|
92
|
+
/** Custom persistence: called on startup to load encrypted blob */
|
|
93
|
+
onLoad?: () => string | null | Promise<string | null>;
|
|
94
|
+
/** File path for persist: 'file' (Node.js environments only) */
|
|
95
|
+
persistPath?: string;
|
|
96
|
+
/** Transport preference for listen() — tries in order, falls back automatically
|
|
97
|
+
* Default: ['sse', 'long-poll']. Options: 'websocket' | 'sse' | 'long-poll' */
|
|
98
|
+
transport?: ('websocket' | 'sse' | 'long-poll')[];
|
|
88
99
|
}
|
|
89
100
|
interface ListenOptions {
|
|
90
101
|
/** Milliseconds between polls (default: 2000, min: 500) */
|
|
@@ -185,6 +196,12 @@ declare class VoidlyAgent {
|
|
|
185
196
|
private _rpcPending;
|
|
186
197
|
private _coverTrafficTimer;
|
|
187
198
|
private _rpcListener;
|
|
199
|
+
private _persistMode;
|
|
200
|
+
private _onPersist?;
|
|
201
|
+
private _onLoad?;
|
|
202
|
+
private _persistPath?;
|
|
203
|
+
private _persistKey;
|
|
204
|
+
private _transportPrefs;
|
|
188
205
|
private constructor();
|
|
189
206
|
/**
|
|
190
207
|
* Register a new agent on the Voidly relay.
|
|
@@ -248,6 +265,26 @@ declare class VoidlyAgent {
|
|
|
248
265
|
signedPrekeyPublic?: string;
|
|
249
266
|
signedPrekeyId?: number;
|
|
250
267
|
};
|
|
268
|
+
/** Derive persistence encryption key from signing secret */
|
|
269
|
+
private _derivePersistKey;
|
|
270
|
+
/** Auto-persist ratchet state (called after every ratchet mutation) */
|
|
271
|
+
private _persistRatchetState;
|
|
272
|
+
/** Load persisted ratchet state and restore into memory */
|
|
273
|
+
private _loadPersistedRatchetState;
|
|
274
|
+
/** IndexedDB put helper (browser only) */
|
|
275
|
+
private _idbPut;
|
|
276
|
+
/** IndexedDB get helper (browser only) */
|
|
277
|
+
private _idbGet;
|
|
278
|
+
/**
|
|
279
|
+
* Force-persist current ratchet state.
|
|
280
|
+
* Useful to call before process exit to ensure state is saved.
|
|
281
|
+
*/
|
|
282
|
+
flushRatchetState(): Promise<void>;
|
|
283
|
+
/**
|
|
284
|
+
* Restore an agent from credentials with async persistence loading.
|
|
285
|
+
* Use this instead of `fromCredentials()` when using file/relay/custom persistence.
|
|
286
|
+
*/
|
|
287
|
+
static fromCredentialsAsync(creds: Parameters<typeof VoidlyAgent.fromCredentials>[0], config?: VoidlyAgentConfig): Promise<VoidlyAgent>;
|
|
251
288
|
/**
|
|
252
289
|
* Get the number of messages that failed to decrypt.
|
|
253
290
|
* Useful for detecting key mismatches, attacks, or corruption.
|
|
@@ -300,6 +337,8 @@ declare class VoidlyAgent {
|
|
|
300
337
|
messageType?: string;
|
|
301
338
|
unreadOnly?: boolean;
|
|
302
339
|
}): Promise<DecryptedMessage[]>;
|
|
340
|
+
/** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports) */
|
|
341
|
+
private _decryptMessages;
|
|
303
342
|
/**
|
|
304
343
|
* Delete a message by ID (must be sender or recipient).
|
|
305
344
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -85,6 +85,17 @@ interface VoidlyAgentConfig {
|
|
|
85
85
|
jitterMs?: number;
|
|
86
86
|
/** Use long-poll for listen() instead of short-interval polling (default: true) */
|
|
87
87
|
longPoll?: boolean;
|
|
88
|
+
/** Auto-persist ratchet state after every send/receive (default: 'memory' = no persistence) */
|
|
89
|
+
persist?: 'memory' | 'localStorage' | 'indexedDB' | 'file' | 'relay' | 'custom';
|
|
90
|
+
/** Custom persistence: called after each ratchet step with encrypted blob */
|
|
91
|
+
onPersist?: (encryptedState: string) => void | Promise<void>;
|
|
92
|
+
/** Custom persistence: called on startup to load encrypted blob */
|
|
93
|
+
onLoad?: () => string | null | Promise<string | null>;
|
|
94
|
+
/** File path for persist: 'file' (Node.js environments only) */
|
|
95
|
+
persistPath?: string;
|
|
96
|
+
/** Transport preference for listen() — tries in order, falls back automatically
|
|
97
|
+
* Default: ['sse', 'long-poll']. Options: 'websocket' | 'sse' | 'long-poll' */
|
|
98
|
+
transport?: ('websocket' | 'sse' | 'long-poll')[];
|
|
88
99
|
}
|
|
89
100
|
interface ListenOptions {
|
|
90
101
|
/** Milliseconds between polls (default: 2000, min: 500) */
|
|
@@ -185,6 +196,12 @@ declare class VoidlyAgent {
|
|
|
185
196
|
private _rpcPending;
|
|
186
197
|
private _coverTrafficTimer;
|
|
187
198
|
private _rpcListener;
|
|
199
|
+
private _persistMode;
|
|
200
|
+
private _onPersist?;
|
|
201
|
+
private _onLoad?;
|
|
202
|
+
private _persistPath?;
|
|
203
|
+
private _persistKey;
|
|
204
|
+
private _transportPrefs;
|
|
188
205
|
private constructor();
|
|
189
206
|
/**
|
|
190
207
|
* Register a new agent on the Voidly relay.
|
|
@@ -248,6 +265,26 @@ declare class VoidlyAgent {
|
|
|
248
265
|
signedPrekeyPublic?: string;
|
|
249
266
|
signedPrekeyId?: number;
|
|
250
267
|
};
|
|
268
|
+
/** Derive persistence encryption key from signing secret */
|
|
269
|
+
private _derivePersistKey;
|
|
270
|
+
/** Auto-persist ratchet state (called after every ratchet mutation) */
|
|
271
|
+
private _persistRatchetState;
|
|
272
|
+
/** Load persisted ratchet state and restore into memory */
|
|
273
|
+
private _loadPersistedRatchetState;
|
|
274
|
+
/** IndexedDB put helper (browser only) */
|
|
275
|
+
private _idbPut;
|
|
276
|
+
/** IndexedDB get helper (browser only) */
|
|
277
|
+
private _idbGet;
|
|
278
|
+
/**
|
|
279
|
+
* Force-persist current ratchet state.
|
|
280
|
+
* Useful to call before process exit to ensure state is saved.
|
|
281
|
+
*/
|
|
282
|
+
flushRatchetState(): Promise<void>;
|
|
283
|
+
/**
|
|
284
|
+
* Restore an agent from credentials with async persistence loading.
|
|
285
|
+
* Use this instead of `fromCredentials()` when using file/relay/custom persistence.
|
|
286
|
+
*/
|
|
287
|
+
static fromCredentialsAsync(creds: Parameters<typeof VoidlyAgent.fromCredentials>[0], config?: VoidlyAgentConfig): Promise<VoidlyAgent>;
|
|
251
288
|
/**
|
|
252
289
|
* Get the number of messages that failed to decrypt.
|
|
253
290
|
* Useful for detecting key mismatches, attacks, or corruption.
|
|
@@ -300,6 +337,8 @@ declare class VoidlyAgent {
|
|
|
300
337
|
messageType?: string;
|
|
301
338
|
unreadOnly?: boolean;
|
|
302
339
|
}): Promise<DecryptedMessage[]>;
|
|
340
|
+
/** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports) */
|
|
341
|
+
private _decryptMessages;
|
|
303
342
|
/**
|
|
304
343
|
* Delete a message by ID (must be sender or recipient).
|
|
305
344
|
*/
|
package/dist/index.js
CHANGED
|
@@ -2230,22 +2230,22 @@ var require_nacl_fast = __commonJS({
|
|
|
2230
2230
|
randombytes = fn;
|
|
2231
2231
|
};
|
|
2232
2232
|
(function() {
|
|
2233
|
-
var
|
|
2234
|
-
if (
|
|
2233
|
+
var crypto = typeof self !== "undefined" ? self.crypto || self.msCrypto : null;
|
|
2234
|
+
if (crypto && crypto.getRandomValues) {
|
|
2235
2235
|
var QUOTA = 65536;
|
|
2236
2236
|
nacl2.setPRNG(function(x, n) {
|
|
2237
2237
|
var i, v = new Uint8Array(n);
|
|
2238
2238
|
for (i = 0; i < n; i += QUOTA) {
|
|
2239
|
-
|
|
2239
|
+
crypto.getRandomValues(v.subarray(i, i + Math.min(n - i, QUOTA)));
|
|
2240
2240
|
}
|
|
2241
2241
|
for (i = 0; i < n; i++) x[i] = v[i];
|
|
2242
2242
|
cleanup(v);
|
|
2243
2243
|
});
|
|
2244
2244
|
} else if (typeof require !== "undefined") {
|
|
2245
|
-
|
|
2246
|
-
if (
|
|
2245
|
+
crypto = require("crypto");
|
|
2246
|
+
if (crypto && crypto.randomBytes) {
|
|
2247
2247
|
nacl2.setPRNG(function(x, n) {
|
|
2248
|
-
var i, v =
|
|
2248
|
+
var i, v = crypto.randomBytes(n);
|
|
2249
2249
|
for (i = 0; i < n; i++) x[i] = v[i];
|
|
2250
2250
|
cleanup(v);
|
|
2251
2251
|
});
|
|
@@ -4091,6 +4091,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4091
4091
|
this._coverTrafficTimer = null;
|
|
4092
4092
|
// RPC listener handle (started on first onInvoke)
|
|
4093
4093
|
this._rpcListener = null;
|
|
4094
|
+
// Persistence (v3.2)
|
|
4095
|
+
this._persistMode = "memory";
|
|
4096
|
+
this._persistKey = null;
|
|
4094
4097
|
this.did = identity.did;
|
|
4095
4098
|
this.apiKey = identity.apiKey;
|
|
4096
4099
|
this.signingKeyPair = identity.signingKeyPair;
|
|
@@ -4110,6 +4113,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4110
4113
|
this.longPoll = config?.longPoll !== false;
|
|
4111
4114
|
this.mlkemPublicKey = identity.mlkemPublicKey || null;
|
|
4112
4115
|
this.mlkemSecretKey = identity.mlkemSecretKey || null;
|
|
4116
|
+
this._persistMode = config?.persist || "memory";
|
|
4117
|
+
this._onPersist = config?.onPersist;
|
|
4118
|
+
this._onLoad = config?.onLoad;
|
|
4119
|
+
this._persistPath = config?.persistPath;
|
|
4120
|
+
this._transportPrefs = config?.transport || ["sse", "long-poll"];
|
|
4113
4121
|
}
|
|
4114
4122
|
// ─── Factory Methods ────────────────────────────────────────────────────────
|
|
4115
4123
|
/**
|
|
@@ -4314,6 +4322,186 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4314
4322
|
} : {}
|
|
4315
4323
|
};
|
|
4316
4324
|
}
|
|
4325
|
+
// ─── Ratchet Persistence (v3.2) ────────────────────────────────────────────
|
|
4326
|
+
/** Derive persistence encryption key from signing secret */
|
|
4327
|
+
_derivePersistKey() {
|
|
4328
|
+
if (this._persistKey) return this._persistKey;
|
|
4329
|
+
const salt = (0, import_tweetnacl_util.decodeUTF8)("voidly-persist-v1");
|
|
4330
|
+
const input = new Uint8Array(this.signingKeyPair.secretKey.length + salt.length);
|
|
4331
|
+
input.set(this.signingKeyPair.secretKey, 0);
|
|
4332
|
+
input.set(salt, this.signingKeyPair.secretKey.length);
|
|
4333
|
+
this._persistKey = import_tweetnacl.default.hash(input).slice(0, 32);
|
|
4334
|
+
return this._persistKey;
|
|
4335
|
+
}
|
|
4336
|
+
/** Auto-persist ratchet state (called after every ratchet mutation) */
|
|
4337
|
+
async _persistRatchetState() {
|
|
4338
|
+
if (this._persistMode === "memory") return;
|
|
4339
|
+
try {
|
|
4340
|
+
const creds = this.exportCredentials();
|
|
4341
|
+
const data = JSON.stringify(creds.ratchetStates || {});
|
|
4342
|
+
const key = this._derivePersistKey();
|
|
4343
|
+
const nonce = import_tweetnacl.default.randomBytes(24);
|
|
4344
|
+
const encrypted = import_tweetnacl.default.secretbox((0, import_tweetnacl_util.decodeUTF8)(data), nonce, key);
|
|
4345
|
+
const blob = JSON.stringify({ n: (0, import_tweetnacl_util.encodeBase64)(nonce), c: (0, import_tweetnacl_util.encodeBase64)(encrypted), v: 1 });
|
|
4346
|
+
switch (this._persistMode) {
|
|
4347
|
+
case "localStorage":
|
|
4348
|
+
if (typeof localStorage !== "undefined") {
|
|
4349
|
+
localStorage.setItem(`voidly-ratchet-${this.did}`, blob);
|
|
4350
|
+
}
|
|
4351
|
+
break;
|
|
4352
|
+
case "indexedDB":
|
|
4353
|
+
await this._idbPut(blob);
|
|
4354
|
+
break;
|
|
4355
|
+
case "file":
|
|
4356
|
+
if (this._persistPath) {
|
|
4357
|
+
const fs = await import("fs/promises");
|
|
4358
|
+
await fs.writeFile(this._persistPath, blob, "utf-8");
|
|
4359
|
+
}
|
|
4360
|
+
break;
|
|
4361
|
+
case "relay":
|
|
4362
|
+
await this._timedFetch(`${this.baseUrl}/v1/agent/memory/ratchet/state`, {
|
|
4363
|
+
method: "PUT",
|
|
4364
|
+
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
4365
|
+
body: JSON.stringify({ value: blob })
|
|
4366
|
+
}).catch(() => {
|
|
4367
|
+
});
|
|
4368
|
+
break;
|
|
4369
|
+
case "custom":
|
|
4370
|
+
if (this._onPersist) await this._onPersist(blob);
|
|
4371
|
+
break;
|
|
4372
|
+
}
|
|
4373
|
+
} catch {
|
|
4374
|
+
}
|
|
4375
|
+
}
|
|
4376
|
+
/** Load persisted ratchet state and restore into memory */
|
|
4377
|
+
async _loadPersistedRatchetState() {
|
|
4378
|
+
if (this._persistMode === "memory") return;
|
|
4379
|
+
let blob = null;
|
|
4380
|
+
try {
|
|
4381
|
+
switch (this._persistMode) {
|
|
4382
|
+
case "localStorage":
|
|
4383
|
+
if (typeof localStorage !== "undefined") {
|
|
4384
|
+
blob = localStorage.getItem(`voidly-ratchet-${this.did}`);
|
|
4385
|
+
}
|
|
4386
|
+
break;
|
|
4387
|
+
case "indexedDB":
|
|
4388
|
+
blob = await this._idbGet();
|
|
4389
|
+
break;
|
|
4390
|
+
case "file":
|
|
4391
|
+
if (this._persistPath) {
|
|
4392
|
+
try {
|
|
4393
|
+
const fs = await import("fs/promises");
|
|
4394
|
+
blob = await fs.readFile(this._persistPath, "utf-8");
|
|
4395
|
+
} catch {
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
break;
|
|
4399
|
+
case "relay":
|
|
4400
|
+
try {
|
|
4401
|
+
const res = await this._timedFetch(`${this.baseUrl}/v1/agent/memory/ratchet/state`, {
|
|
4402
|
+
headers: { "X-Agent-Key": this.apiKey }
|
|
4403
|
+
});
|
|
4404
|
+
if (res.ok) {
|
|
4405
|
+
const data = await res.json();
|
|
4406
|
+
blob = data.value;
|
|
4407
|
+
}
|
|
4408
|
+
} catch {
|
|
4409
|
+
}
|
|
4410
|
+
break;
|
|
4411
|
+
case "custom":
|
|
4412
|
+
if (this._onLoad) blob = await this._onLoad();
|
|
4413
|
+
break;
|
|
4414
|
+
}
|
|
4415
|
+
} catch {
|
|
4416
|
+
return;
|
|
4417
|
+
}
|
|
4418
|
+
if (!blob) return;
|
|
4419
|
+
try {
|
|
4420
|
+
const { n, c } = JSON.parse(blob);
|
|
4421
|
+
const key = this._derivePersistKey();
|
|
4422
|
+
const decrypted = import_tweetnacl.default.secretbox.open((0, import_tweetnacl_util.decodeBase64)(c), (0, import_tweetnacl_util.decodeBase64)(n), key);
|
|
4423
|
+
if (!decrypted) return;
|
|
4424
|
+
const states = JSON.parse((0, import_tweetnacl_util.encodeUTF8)(decrypted));
|
|
4425
|
+
for (const [pairId, rs] of Object.entries(states)) {
|
|
4426
|
+
if (this._ratchetStates.has(pairId)) continue;
|
|
4427
|
+
const state = {
|
|
4428
|
+
sendChainKey: (0, import_tweetnacl_util.decodeBase64)(rs.sendChainKey),
|
|
4429
|
+
sendStep: rs.sendStep,
|
|
4430
|
+
recvChainKey: (0, import_tweetnacl_util.decodeBase64)(rs.recvChainKey),
|
|
4431
|
+
recvStep: rs.recvStep,
|
|
4432
|
+
skippedKeys: /* @__PURE__ */ new Map()
|
|
4433
|
+
};
|
|
4434
|
+
if (rs.rootKey) state.rootKey = (0, import_tweetnacl_util.decodeBase64)(rs.rootKey);
|
|
4435
|
+
if (rs.dhSendSecretKey && rs.dhSendPublicKey) {
|
|
4436
|
+
state.dhSendKeyPair = {
|
|
4437
|
+
secretKey: (0, import_tweetnacl_util.decodeBase64)(rs.dhSendSecretKey),
|
|
4438
|
+
publicKey: (0, import_tweetnacl_util.decodeBase64)(rs.dhSendPublicKey)
|
|
4439
|
+
};
|
|
4440
|
+
}
|
|
4441
|
+
if (rs.dhRecvPubKey) state.dhRecvPubKey = (0, import_tweetnacl_util.decodeBase64)(rs.dhRecvPubKey);
|
|
4442
|
+
if (rs.prevSendStep !== void 0) state.prevSendStep = rs.prevSendStep;
|
|
4443
|
+
if (state.sendChainKey.length === 32 && state.recvChainKey.length === 32) {
|
|
4444
|
+
this._ratchetStates.set(pairId, state);
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
} catch {
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
/** IndexedDB put helper (browser only) */
|
|
4451
|
+
async _idbPut(blob) {
|
|
4452
|
+
if (typeof indexedDB === "undefined") return;
|
|
4453
|
+
return new Promise((resolve, reject) => {
|
|
4454
|
+
const req = indexedDB.open("voidly-agent", 1);
|
|
4455
|
+
req.onupgradeneeded = () => {
|
|
4456
|
+
const db = req.result;
|
|
4457
|
+
if (!db.objectStoreNames.contains("ratchet")) db.createObjectStore("ratchet");
|
|
4458
|
+
};
|
|
4459
|
+
req.onsuccess = () => {
|
|
4460
|
+
const tx = req.result.transaction("ratchet", "readwrite");
|
|
4461
|
+
tx.objectStore("ratchet").put(blob, this.did);
|
|
4462
|
+
tx.oncomplete = () => resolve();
|
|
4463
|
+
tx.onerror = () => reject(tx.error);
|
|
4464
|
+
};
|
|
4465
|
+
req.onerror = () => reject(req.error);
|
|
4466
|
+
});
|
|
4467
|
+
}
|
|
4468
|
+
/** IndexedDB get helper (browser only) */
|
|
4469
|
+
async _idbGet() {
|
|
4470
|
+
if (typeof indexedDB === "undefined") return null;
|
|
4471
|
+
return new Promise((resolve, reject) => {
|
|
4472
|
+
const req = indexedDB.open("voidly-agent", 1);
|
|
4473
|
+
req.onupgradeneeded = () => {
|
|
4474
|
+
const db = req.result;
|
|
4475
|
+
if (!db.objectStoreNames.contains("ratchet")) db.createObjectStore("ratchet");
|
|
4476
|
+
};
|
|
4477
|
+
req.onsuccess = () => {
|
|
4478
|
+
const tx = req.result.transaction("ratchet", "readonly");
|
|
4479
|
+
const getReq = tx.objectStore("ratchet").get(this.did);
|
|
4480
|
+
getReq.onsuccess = () => resolve(getReq.result || null);
|
|
4481
|
+
getReq.onerror = () => reject(getReq.error);
|
|
4482
|
+
};
|
|
4483
|
+
req.onerror = () => reject(req.error);
|
|
4484
|
+
});
|
|
4485
|
+
}
|
|
4486
|
+
/**
|
|
4487
|
+
* Force-persist current ratchet state.
|
|
4488
|
+
* Useful to call before process exit to ensure state is saved.
|
|
4489
|
+
*/
|
|
4490
|
+
async flushRatchetState() {
|
|
4491
|
+
const origMode = this._persistMode;
|
|
4492
|
+
if (origMode === "memory") this._persistMode = "file";
|
|
4493
|
+
await this._persistRatchetState();
|
|
4494
|
+
this._persistMode = origMode;
|
|
4495
|
+
}
|
|
4496
|
+
/**
|
|
4497
|
+
* Restore an agent from credentials with async persistence loading.
|
|
4498
|
+
* Use this instead of `fromCredentials()` when using file/relay/custom persistence.
|
|
4499
|
+
*/
|
|
4500
|
+
static async fromCredentialsAsync(creds, config) {
|
|
4501
|
+
const agent = _VoidlyAgent.fromCredentials(creds, config);
|
|
4502
|
+
await agent._loadPersistedRatchetState();
|
|
4503
|
+
return agent;
|
|
4504
|
+
}
|
|
4317
4505
|
/**
|
|
4318
4506
|
* Get the number of messages that failed to decrypt.
|
|
4319
4507
|
* Useful for detecting key mismatches, attacks, or corruption.
|
|
@@ -4390,7 +4578,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4390
4578
|
const combined = new Uint8Array(x25519Shared.length + pqShared.length);
|
|
4391
4579
|
combined.set(x25519Shared, 0);
|
|
4392
4580
|
combined.set(pqShared, x25519Shared.length);
|
|
4393
|
-
initialKey = new Uint8Array(await crypto.subtle.digest("SHA-256", combined));
|
|
4581
|
+
initialKey = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", combined));
|
|
4394
4582
|
} catch {
|
|
4395
4583
|
initialKey = x25519Shared;
|
|
4396
4584
|
}
|
|
@@ -4489,6 +4677,8 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4489
4677
|
payload.thread_id = options.threadId;
|
|
4490
4678
|
payload.reply_to = options.replyTo;
|
|
4491
4679
|
}
|
|
4680
|
+
this._persistRatchetState().catch(() => {
|
|
4681
|
+
});
|
|
4492
4682
|
const relays = [this.baseUrl, ...this.fallbackRelays];
|
|
4493
4683
|
let lastError = null;
|
|
4494
4684
|
for (const relay of relays) {
|
|
@@ -4547,8 +4737,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4547
4737
|
throw new Error(`Receive failed: ${err.error?.message || err.error || res.statusText}`);
|
|
4548
4738
|
}
|
|
4549
4739
|
const data = await res.json();
|
|
4740
|
+
return this._decryptMessages(data.messages);
|
|
4741
|
+
}
|
|
4742
|
+
/** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports) */
|
|
4743
|
+
async _decryptMessages(rawMessages) {
|
|
4550
4744
|
const decrypted = [];
|
|
4551
|
-
for (const msg of
|
|
4745
|
+
for (const msg of rawMessages) {
|
|
4552
4746
|
try {
|
|
4553
4747
|
if (this._seenMessageIds.has(msg.id)) continue;
|
|
4554
4748
|
let senderEncPub;
|
|
@@ -4572,7 +4766,6 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4572
4766
|
let envelopePqCiphertext = null;
|
|
4573
4767
|
let envelopeDhRatchetKey = null;
|
|
4574
4768
|
let envelopePn = 0;
|
|
4575
|
-
let envelopeDeniable = false;
|
|
4576
4769
|
if (msg.envelope) {
|
|
4577
4770
|
try {
|
|
4578
4771
|
const env = JSON.parse(msg.envelope);
|
|
@@ -4605,7 +4798,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4605
4798
|
const combined = new Uint8Array(x25519Shared.length + pqShared.length);
|
|
4606
4799
|
combined.set(x25519Shared, 0);
|
|
4607
4800
|
combined.set(pqShared, x25519Shared.length);
|
|
4608
|
-
initialKey = new Uint8Array(await crypto.subtle.digest("SHA-256", combined));
|
|
4801
|
+
initialKey = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", combined));
|
|
4609
4802
|
} catch {
|
|
4610
4803
|
initialKey = x25519Shared;
|
|
4611
4804
|
}
|
|
@@ -4802,6 +4995,10 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4802
4995
|
this._decryptFailCount++;
|
|
4803
4996
|
}
|
|
4804
4997
|
}
|
|
4998
|
+
if (decrypted.length > 0) {
|
|
4999
|
+
this._persistRatchetState().catch(() => {
|
|
5000
|
+
});
|
|
5001
|
+
}
|
|
4805
5002
|
return decrypted;
|
|
4806
5003
|
}
|
|
4807
5004
|
// ─── Message Management ─────────────────────────────────────────────────────
|
|
@@ -6090,6 +6287,114 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6090
6287
|
this.ping().catch(() => {
|
|
6091
6288
|
});
|
|
6092
6289
|
}
|
|
6290
|
+
const deliverMessages = async (messages) => {
|
|
6291
|
+
for (const msg of messages) {
|
|
6292
|
+
try {
|
|
6293
|
+
await onMessage(msg);
|
|
6294
|
+
if (autoMarkRead) {
|
|
6295
|
+
await this.markRead(msg.id).catch(() => {
|
|
6296
|
+
});
|
|
6297
|
+
}
|
|
6298
|
+
} catch (err) {
|
|
6299
|
+
if (onError) onError(err instanceof Error ? err : new Error(String(err)));
|
|
6300
|
+
}
|
|
6301
|
+
}
|
|
6302
|
+
if (messages.length > 0) {
|
|
6303
|
+
lastSeen = messages[messages.length - 1].timestamp;
|
|
6304
|
+
}
|
|
6305
|
+
};
|
|
6306
|
+
let lastEventId = "";
|
|
6307
|
+
let sseFailures = 0;
|
|
6308
|
+
const startSSE = async () => {
|
|
6309
|
+
try {
|
|
6310
|
+
const params = new URLSearchParams();
|
|
6311
|
+
if (lastSeen) params.set("since", lastSeen);
|
|
6312
|
+
if (options.from) params.set("from", options.from);
|
|
6313
|
+
const sseUrl = `${this.baseUrl}/v1/agent/receive/sse?${params}`;
|
|
6314
|
+
const headers = { "X-Agent-Key": this.apiKey };
|
|
6315
|
+
if (lastEventId) headers["Last-Event-ID"] = lastEventId;
|
|
6316
|
+
const controller = new AbortController();
|
|
6317
|
+
const sseTimeout = setTimeout(() => controller.abort(), 6e4);
|
|
6318
|
+
let res;
|
|
6319
|
+
try {
|
|
6320
|
+
res = await fetch(sseUrl, { headers, signal: controller.signal });
|
|
6321
|
+
} catch (e) {
|
|
6322
|
+
clearTimeout(sseTimeout);
|
|
6323
|
+
throw e;
|
|
6324
|
+
}
|
|
6325
|
+
if (!res.ok || !res.body) {
|
|
6326
|
+
clearTimeout(sseTimeout);
|
|
6327
|
+
return false;
|
|
6328
|
+
}
|
|
6329
|
+
const reader = res.body.getReader();
|
|
6330
|
+
const decoder = new TextDecoder();
|
|
6331
|
+
let buffer = "";
|
|
6332
|
+
try {
|
|
6333
|
+
while (active && !options.signal?.aborted) {
|
|
6334
|
+
const { done: streamDone, value } = await reader.read();
|
|
6335
|
+
if (streamDone) break;
|
|
6336
|
+
buffer += decoder.decode(value, { stream: true });
|
|
6337
|
+
const lines = buffer.split("\n");
|
|
6338
|
+
buffer = lines.pop() || "";
|
|
6339
|
+
let eventType = "";
|
|
6340
|
+
let dataStr = "";
|
|
6341
|
+
let eventId = "";
|
|
6342
|
+
for (const line of lines) {
|
|
6343
|
+
if (line.startsWith("event: ")) {
|
|
6344
|
+
eventType = line.slice(7).trim();
|
|
6345
|
+
} else if (line.startsWith("data: ")) {
|
|
6346
|
+
dataStr += (dataStr ? "\n" : "") + line.slice(6);
|
|
6347
|
+
} else if (line.startsWith("id: ")) {
|
|
6348
|
+
eventId = line.slice(4).trim();
|
|
6349
|
+
} else if (line === "" && (dataStr || eventType)) {
|
|
6350
|
+
if (eventId) lastEventId = eventId;
|
|
6351
|
+
if (eventType === "message" && dataStr) {
|
|
6352
|
+
try {
|
|
6353
|
+
const rawMsg = JSON.parse(dataStr);
|
|
6354
|
+
const decrypted = await this._decryptMessages([rawMsg]);
|
|
6355
|
+
if (decrypted.length > 0) {
|
|
6356
|
+
consecutiveEmpty = 0;
|
|
6357
|
+
sseFailures = 0;
|
|
6358
|
+
await deliverMessages(decrypted);
|
|
6359
|
+
}
|
|
6360
|
+
} catch {
|
|
6361
|
+
}
|
|
6362
|
+
} else if (eventType === "reconnect") {
|
|
6363
|
+
break;
|
|
6364
|
+
}
|
|
6365
|
+
eventType = "";
|
|
6366
|
+
dataStr = "";
|
|
6367
|
+
eventId = "";
|
|
6368
|
+
}
|
|
6369
|
+
}
|
|
6370
|
+
}
|
|
6371
|
+
} finally {
|
|
6372
|
+
reader.releaseLock();
|
|
6373
|
+
clearTimeout(sseTimeout);
|
|
6374
|
+
}
|
|
6375
|
+
sseFailures = 0;
|
|
6376
|
+
return true;
|
|
6377
|
+
} catch {
|
|
6378
|
+
sseFailures++;
|
|
6379
|
+
return false;
|
|
6380
|
+
}
|
|
6381
|
+
};
|
|
6382
|
+
const sseLoop = async () => {
|
|
6383
|
+
while (active && !options.signal?.aborted) {
|
|
6384
|
+
const ok = await startSSE();
|
|
6385
|
+
if (!active || options.signal?.aborted) break;
|
|
6386
|
+
if (!ok) {
|
|
6387
|
+
if (sseFailures >= 3) {
|
|
6388
|
+
poll();
|
|
6389
|
+
return;
|
|
6390
|
+
}
|
|
6391
|
+
const backoff = Math.min(1e3 * Math.pow(2, sseFailures - 1), 4e3);
|
|
6392
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
6393
|
+
continue;
|
|
6394
|
+
}
|
|
6395
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
6396
|
+
}
|
|
6397
|
+
};
|
|
6093
6398
|
const useLongPoll = this.longPoll;
|
|
6094
6399
|
const poll = async () => {
|
|
6095
6400
|
if (!active || options.signal?.aborted) {
|
|
@@ -6097,62 +6402,18 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6097
6402
|
return;
|
|
6098
6403
|
}
|
|
6099
6404
|
try {
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
if (res.ok) {
|
|
6109
|
-
const data = await res.json();
|
|
6110
|
-
messages = [];
|
|
6111
|
-
for (const raw of data.messages) {
|
|
6112
|
-
try {
|
|
6113
|
-
if (this._seenMessageIds.has(raw.id)) continue;
|
|
6114
|
-
this._seenMessageIds.add(raw.id);
|
|
6115
|
-
} catch {
|
|
6116
|
-
}
|
|
6117
|
-
}
|
|
6118
|
-
if (data.messages.length > 0) {
|
|
6119
|
-
messages = await this.receive({
|
|
6120
|
-
since: lastSeen,
|
|
6121
|
-
from: options.from,
|
|
6122
|
-
threadId: options.threadId,
|
|
6123
|
-
messageType: options.messageType,
|
|
6124
|
-
unreadOnly,
|
|
6125
|
-
limit: 50
|
|
6126
|
-
});
|
|
6127
|
-
}
|
|
6128
|
-
} else {
|
|
6129
|
-
messages = [];
|
|
6130
|
-
}
|
|
6131
|
-
} else {
|
|
6132
|
-
messages = await this.receive({
|
|
6133
|
-
since: lastSeen,
|
|
6134
|
-
from: options.from,
|
|
6135
|
-
threadId: options.threadId,
|
|
6136
|
-
messageType: options.messageType,
|
|
6137
|
-
unreadOnly,
|
|
6138
|
-
limit: 50
|
|
6139
|
-
});
|
|
6140
|
-
}
|
|
6405
|
+
const messages = await this.receive({
|
|
6406
|
+
since: lastSeen,
|
|
6407
|
+
from: options.from,
|
|
6408
|
+
threadId: options.threadId,
|
|
6409
|
+
messageType: options.messageType,
|
|
6410
|
+
unreadOnly,
|
|
6411
|
+
limit: 50
|
|
6412
|
+
});
|
|
6141
6413
|
if (messages.length > 0) {
|
|
6142
6414
|
consecutiveEmpty = 0;
|
|
6143
6415
|
if (adaptive) currentInterval = Math.max(interval / 2, 500);
|
|
6144
|
-
|
|
6145
|
-
try {
|
|
6146
|
-
await onMessage(msg);
|
|
6147
|
-
if (autoMarkRead) {
|
|
6148
|
-
await this.markRead(msg.id).catch(() => {
|
|
6149
|
-
});
|
|
6150
|
-
}
|
|
6151
|
-
} catch (err) {
|
|
6152
|
-
if (onError) onError(err instanceof Error ? err : new Error(String(err)));
|
|
6153
|
-
}
|
|
6154
|
-
}
|
|
6155
|
-
lastSeen = messages[messages.length - 1].timestamp;
|
|
6416
|
+
await deliverMessages(messages);
|
|
6156
6417
|
} else {
|
|
6157
6418
|
consecutiveEmpty++;
|
|
6158
6419
|
if (adaptive && consecutiveEmpty > 3 && !useLongPoll) {
|
|
@@ -6167,7 +6428,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6167
6428
|
timer = setTimeout(poll, useLongPoll ? 100 : currentInterval);
|
|
6168
6429
|
}
|
|
6169
6430
|
};
|
|
6170
|
-
|
|
6431
|
+
const prefs = this._transportPrefs;
|
|
6432
|
+
if (prefs.includes("sse")) {
|
|
6433
|
+
sseLoop();
|
|
6434
|
+
} else {
|
|
6435
|
+
poll();
|
|
6436
|
+
}
|
|
6171
6437
|
return handle;
|
|
6172
6438
|
}
|
|
6173
6439
|
/**
|
|
@@ -6499,7 +6765,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6499
6765
|
for (const relay of relays) {
|
|
6500
6766
|
try {
|
|
6501
6767
|
const res = await this._timedFetch(`${relay}${path}`, init);
|
|
6502
|
-
if (res.ok || res.status >= 400 && res.status < 500) return res;
|
|
6768
|
+
if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) return res;
|
|
6769
|
+
if (res.status === 429) {
|
|
6770
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
6771
|
+
const waitMs = retryAfter ? Math.min(parseInt(retryAfter, 10) * 1e3, 5e3) : 1e3;
|
|
6772
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
6773
|
+
}
|
|
6503
6774
|
} catch (err) {
|
|
6504
6775
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
6505
6776
|
}
|
|
@@ -6686,7 +6957,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6686
6957
|
...this.requireSignatures ? ["Strict signature enforcement (reject unsigned/invalid messages)"] : [],
|
|
6687
6958
|
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays \u2014 receive, discover, identity all use fallbacks)`] : [],
|
|
6688
6959
|
...this.jitterMs > 0 ? [`Timing jitter (random ${this.jitterMs}ms delay \u2014 metadata timing protection)`] : [],
|
|
6960
|
+
...this._transportPrefs.includes("sse") ? ["SSE streaming transport (real-time push delivery from relay)"] : [],
|
|
6689
6961
|
...this.longPoll ? ["Long-poll transport (25s server-held connection \u2014 near-real-time delivery)"] : [],
|
|
6962
|
+
...this._persistMode !== "memory" ? [`Ratchet state auto-persistence (${this._persistMode} backend \u2014 survives process restart)`] : [],
|
|
6690
6963
|
...this._coverTrafficTimer !== null ? ["Cover traffic (encrypted noise at random intervals \u2014 traffic analysis resistance)"] : [],
|
|
6691
6964
|
"Agent RPC (invoke/onInvoke \u2014 synchronous function calls between agents)",
|
|
6692
6965
|
"P2P direct send (bypass relay via webhook \u2014 true peer-to-peer when possible)",
|
|
@@ -6700,7 +6973,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6700
6973
|
...this.sealedSender ? ["Relay sees to_did (needed for routing) but NOT from_did, thread_id, or message_type"] : ["Relay sees from_did, to_did, thread_id, message_type in cleartext \u2014 enable sealedSender to strip metadata"],
|
|
6701
6974
|
"Relay sees channel membership, task delegation, trust scores (social graph)",
|
|
6702
6975
|
...this.fallbackRelays.length === 0 ? ["Single relay with no fallbacks \u2014 configure fallbackRelays for resilience"] : [],
|
|
6703
|
-
"Ratchet state is in-memory (lost on process restart \u2014
|
|
6976
|
+
...this._persistMode === "memory" ? ["Ratchet state is in-memory (lost on process restart \u2014 use persist option or exportCredentials)"] : [],
|
|
6704
6977
|
...!this.deniable ? ["Ed25519 signatures are non-repudiable \u2014 enable deniable option for HMAC auth"] : [],
|
|
6705
6978
|
...!this.doubleRatchet ? ["Hash ratchet only \u2014 enable doubleRatchet option for post-compromise recovery"] : [],
|
|
6706
6979
|
...this.jitterMs === 0 ? ["No timing jitter \u2014 enable jitterMs option for metadata protection"] : [],
|
package/dist/index.mjs
CHANGED
|
@@ -2230,22 +2230,22 @@ var require_nacl_fast = __commonJS({
|
|
|
2230
2230
|
randombytes = fn;
|
|
2231
2231
|
};
|
|
2232
2232
|
(function() {
|
|
2233
|
-
var
|
|
2234
|
-
if (
|
|
2233
|
+
var crypto = typeof self !== "undefined" ? self.crypto || self.msCrypto : null;
|
|
2234
|
+
if (crypto && crypto.getRandomValues) {
|
|
2235
2235
|
var QUOTA = 65536;
|
|
2236
2236
|
nacl2.setPRNG(function(x, n) {
|
|
2237
2237
|
var i, v = new Uint8Array(n);
|
|
2238
2238
|
for (i = 0; i < n; i += QUOTA) {
|
|
2239
|
-
|
|
2239
|
+
crypto.getRandomValues(v.subarray(i, i + Math.min(n - i, QUOTA)));
|
|
2240
2240
|
}
|
|
2241
2241
|
for (i = 0; i < n; i++) x[i] = v[i];
|
|
2242
2242
|
cleanup(v);
|
|
2243
2243
|
});
|
|
2244
2244
|
} else if (typeof __require !== "undefined") {
|
|
2245
|
-
|
|
2246
|
-
if (
|
|
2245
|
+
crypto = __require("crypto");
|
|
2246
|
+
if (crypto && crypto.randomBytes) {
|
|
2247
2247
|
nacl2.setPRNG(function(x, n) {
|
|
2248
|
-
var i, v =
|
|
2248
|
+
var i, v = crypto.randomBytes(n);
|
|
2249
2249
|
for (i = 0; i < n; i++) x[i] = v[i];
|
|
2250
2250
|
cleanup(v);
|
|
2251
2251
|
});
|
|
@@ -4080,6 +4080,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4080
4080
|
this._coverTrafficTimer = null;
|
|
4081
4081
|
// RPC listener handle (started on first onInvoke)
|
|
4082
4082
|
this._rpcListener = null;
|
|
4083
|
+
// Persistence (v3.2)
|
|
4084
|
+
this._persistMode = "memory";
|
|
4085
|
+
this._persistKey = null;
|
|
4083
4086
|
this.did = identity.did;
|
|
4084
4087
|
this.apiKey = identity.apiKey;
|
|
4085
4088
|
this.signingKeyPair = identity.signingKeyPair;
|
|
@@ -4099,6 +4102,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4099
4102
|
this.longPoll = config?.longPoll !== false;
|
|
4100
4103
|
this.mlkemPublicKey = identity.mlkemPublicKey || null;
|
|
4101
4104
|
this.mlkemSecretKey = identity.mlkemSecretKey || null;
|
|
4105
|
+
this._persistMode = config?.persist || "memory";
|
|
4106
|
+
this._onPersist = config?.onPersist;
|
|
4107
|
+
this._onLoad = config?.onLoad;
|
|
4108
|
+
this._persistPath = config?.persistPath;
|
|
4109
|
+
this._transportPrefs = config?.transport || ["sse", "long-poll"];
|
|
4102
4110
|
}
|
|
4103
4111
|
// ─── Factory Methods ────────────────────────────────────────────────────────
|
|
4104
4112
|
/**
|
|
@@ -4303,6 +4311,186 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4303
4311
|
} : {}
|
|
4304
4312
|
};
|
|
4305
4313
|
}
|
|
4314
|
+
// ─── Ratchet Persistence (v3.2) ────────────────────────────────────────────
|
|
4315
|
+
/** Derive persistence encryption key from signing secret */
|
|
4316
|
+
_derivePersistKey() {
|
|
4317
|
+
if (this._persistKey) return this._persistKey;
|
|
4318
|
+
const salt = (0, import_tweetnacl_util.decodeUTF8)("voidly-persist-v1");
|
|
4319
|
+
const input = new Uint8Array(this.signingKeyPair.secretKey.length + salt.length);
|
|
4320
|
+
input.set(this.signingKeyPair.secretKey, 0);
|
|
4321
|
+
input.set(salt, this.signingKeyPair.secretKey.length);
|
|
4322
|
+
this._persistKey = import_tweetnacl.default.hash(input).slice(0, 32);
|
|
4323
|
+
return this._persistKey;
|
|
4324
|
+
}
|
|
4325
|
+
/** Auto-persist ratchet state (called after every ratchet mutation) */
|
|
4326
|
+
async _persistRatchetState() {
|
|
4327
|
+
if (this._persistMode === "memory") return;
|
|
4328
|
+
try {
|
|
4329
|
+
const creds = this.exportCredentials();
|
|
4330
|
+
const data = JSON.stringify(creds.ratchetStates || {});
|
|
4331
|
+
const key = this._derivePersistKey();
|
|
4332
|
+
const nonce = import_tweetnacl.default.randomBytes(24);
|
|
4333
|
+
const encrypted = import_tweetnacl.default.secretbox((0, import_tweetnacl_util.decodeUTF8)(data), nonce, key);
|
|
4334
|
+
const blob = JSON.stringify({ n: (0, import_tweetnacl_util.encodeBase64)(nonce), c: (0, import_tweetnacl_util.encodeBase64)(encrypted), v: 1 });
|
|
4335
|
+
switch (this._persistMode) {
|
|
4336
|
+
case "localStorage":
|
|
4337
|
+
if (typeof localStorage !== "undefined") {
|
|
4338
|
+
localStorage.setItem(`voidly-ratchet-${this.did}`, blob);
|
|
4339
|
+
}
|
|
4340
|
+
break;
|
|
4341
|
+
case "indexedDB":
|
|
4342
|
+
await this._idbPut(blob);
|
|
4343
|
+
break;
|
|
4344
|
+
case "file":
|
|
4345
|
+
if (this._persistPath) {
|
|
4346
|
+
const fs = await import("fs/promises");
|
|
4347
|
+
await fs.writeFile(this._persistPath, blob, "utf-8");
|
|
4348
|
+
}
|
|
4349
|
+
break;
|
|
4350
|
+
case "relay":
|
|
4351
|
+
await this._timedFetch(`${this.baseUrl}/v1/agent/memory/ratchet/state`, {
|
|
4352
|
+
method: "PUT",
|
|
4353
|
+
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
4354
|
+
body: JSON.stringify({ value: blob })
|
|
4355
|
+
}).catch(() => {
|
|
4356
|
+
});
|
|
4357
|
+
break;
|
|
4358
|
+
case "custom":
|
|
4359
|
+
if (this._onPersist) await this._onPersist(blob);
|
|
4360
|
+
break;
|
|
4361
|
+
}
|
|
4362
|
+
} catch {
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
/** Load persisted ratchet state and restore into memory */
|
|
4366
|
+
async _loadPersistedRatchetState() {
|
|
4367
|
+
if (this._persistMode === "memory") return;
|
|
4368
|
+
let blob = null;
|
|
4369
|
+
try {
|
|
4370
|
+
switch (this._persistMode) {
|
|
4371
|
+
case "localStorage":
|
|
4372
|
+
if (typeof localStorage !== "undefined") {
|
|
4373
|
+
blob = localStorage.getItem(`voidly-ratchet-${this.did}`);
|
|
4374
|
+
}
|
|
4375
|
+
break;
|
|
4376
|
+
case "indexedDB":
|
|
4377
|
+
blob = await this._idbGet();
|
|
4378
|
+
break;
|
|
4379
|
+
case "file":
|
|
4380
|
+
if (this._persistPath) {
|
|
4381
|
+
try {
|
|
4382
|
+
const fs = await import("fs/promises");
|
|
4383
|
+
blob = await fs.readFile(this._persistPath, "utf-8");
|
|
4384
|
+
} catch {
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
break;
|
|
4388
|
+
case "relay":
|
|
4389
|
+
try {
|
|
4390
|
+
const res = await this._timedFetch(`${this.baseUrl}/v1/agent/memory/ratchet/state`, {
|
|
4391
|
+
headers: { "X-Agent-Key": this.apiKey }
|
|
4392
|
+
});
|
|
4393
|
+
if (res.ok) {
|
|
4394
|
+
const data = await res.json();
|
|
4395
|
+
blob = data.value;
|
|
4396
|
+
}
|
|
4397
|
+
} catch {
|
|
4398
|
+
}
|
|
4399
|
+
break;
|
|
4400
|
+
case "custom":
|
|
4401
|
+
if (this._onLoad) blob = await this._onLoad();
|
|
4402
|
+
break;
|
|
4403
|
+
}
|
|
4404
|
+
} catch {
|
|
4405
|
+
return;
|
|
4406
|
+
}
|
|
4407
|
+
if (!blob) return;
|
|
4408
|
+
try {
|
|
4409
|
+
const { n, c } = JSON.parse(blob);
|
|
4410
|
+
const key = this._derivePersistKey();
|
|
4411
|
+
const decrypted = import_tweetnacl.default.secretbox.open((0, import_tweetnacl_util.decodeBase64)(c), (0, import_tweetnacl_util.decodeBase64)(n), key);
|
|
4412
|
+
if (!decrypted) return;
|
|
4413
|
+
const states = JSON.parse((0, import_tweetnacl_util.encodeUTF8)(decrypted));
|
|
4414
|
+
for (const [pairId, rs] of Object.entries(states)) {
|
|
4415
|
+
if (this._ratchetStates.has(pairId)) continue;
|
|
4416
|
+
const state = {
|
|
4417
|
+
sendChainKey: (0, import_tweetnacl_util.decodeBase64)(rs.sendChainKey),
|
|
4418
|
+
sendStep: rs.sendStep,
|
|
4419
|
+
recvChainKey: (0, import_tweetnacl_util.decodeBase64)(rs.recvChainKey),
|
|
4420
|
+
recvStep: rs.recvStep,
|
|
4421
|
+
skippedKeys: /* @__PURE__ */ new Map()
|
|
4422
|
+
};
|
|
4423
|
+
if (rs.rootKey) state.rootKey = (0, import_tweetnacl_util.decodeBase64)(rs.rootKey);
|
|
4424
|
+
if (rs.dhSendSecretKey && rs.dhSendPublicKey) {
|
|
4425
|
+
state.dhSendKeyPair = {
|
|
4426
|
+
secretKey: (0, import_tweetnacl_util.decodeBase64)(rs.dhSendSecretKey),
|
|
4427
|
+
publicKey: (0, import_tweetnacl_util.decodeBase64)(rs.dhSendPublicKey)
|
|
4428
|
+
};
|
|
4429
|
+
}
|
|
4430
|
+
if (rs.dhRecvPubKey) state.dhRecvPubKey = (0, import_tweetnacl_util.decodeBase64)(rs.dhRecvPubKey);
|
|
4431
|
+
if (rs.prevSendStep !== void 0) state.prevSendStep = rs.prevSendStep;
|
|
4432
|
+
if (state.sendChainKey.length === 32 && state.recvChainKey.length === 32) {
|
|
4433
|
+
this._ratchetStates.set(pairId, state);
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
} catch {
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
/** IndexedDB put helper (browser only) */
|
|
4440
|
+
async _idbPut(blob) {
|
|
4441
|
+
if (typeof indexedDB === "undefined") return;
|
|
4442
|
+
return new Promise((resolve, reject) => {
|
|
4443
|
+
const req = indexedDB.open("voidly-agent", 1);
|
|
4444
|
+
req.onupgradeneeded = () => {
|
|
4445
|
+
const db = req.result;
|
|
4446
|
+
if (!db.objectStoreNames.contains("ratchet")) db.createObjectStore("ratchet");
|
|
4447
|
+
};
|
|
4448
|
+
req.onsuccess = () => {
|
|
4449
|
+
const tx = req.result.transaction("ratchet", "readwrite");
|
|
4450
|
+
tx.objectStore("ratchet").put(blob, this.did);
|
|
4451
|
+
tx.oncomplete = () => resolve();
|
|
4452
|
+
tx.onerror = () => reject(tx.error);
|
|
4453
|
+
};
|
|
4454
|
+
req.onerror = () => reject(req.error);
|
|
4455
|
+
});
|
|
4456
|
+
}
|
|
4457
|
+
/** IndexedDB get helper (browser only) */
|
|
4458
|
+
async _idbGet() {
|
|
4459
|
+
if (typeof indexedDB === "undefined") return null;
|
|
4460
|
+
return new Promise((resolve, reject) => {
|
|
4461
|
+
const req = indexedDB.open("voidly-agent", 1);
|
|
4462
|
+
req.onupgradeneeded = () => {
|
|
4463
|
+
const db = req.result;
|
|
4464
|
+
if (!db.objectStoreNames.contains("ratchet")) db.createObjectStore("ratchet");
|
|
4465
|
+
};
|
|
4466
|
+
req.onsuccess = () => {
|
|
4467
|
+
const tx = req.result.transaction("ratchet", "readonly");
|
|
4468
|
+
const getReq = tx.objectStore("ratchet").get(this.did);
|
|
4469
|
+
getReq.onsuccess = () => resolve(getReq.result || null);
|
|
4470
|
+
getReq.onerror = () => reject(getReq.error);
|
|
4471
|
+
};
|
|
4472
|
+
req.onerror = () => reject(req.error);
|
|
4473
|
+
});
|
|
4474
|
+
}
|
|
4475
|
+
/**
|
|
4476
|
+
* Force-persist current ratchet state.
|
|
4477
|
+
* Useful to call before process exit to ensure state is saved.
|
|
4478
|
+
*/
|
|
4479
|
+
async flushRatchetState() {
|
|
4480
|
+
const origMode = this._persistMode;
|
|
4481
|
+
if (origMode === "memory") this._persistMode = "file";
|
|
4482
|
+
await this._persistRatchetState();
|
|
4483
|
+
this._persistMode = origMode;
|
|
4484
|
+
}
|
|
4485
|
+
/**
|
|
4486
|
+
* Restore an agent from credentials with async persistence loading.
|
|
4487
|
+
* Use this instead of `fromCredentials()` when using file/relay/custom persistence.
|
|
4488
|
+
*/
|
|
4489
|
+
static async fromCredentialsAsync(creds, config) {
|
|
4490
|
+
const agent = _VoidlyAgent.fromCredentials(creds, config);
|
|
4491
|
+
await agent._loadPersistedRatchetState();
|
|
4492
|
+
return agent;
|
|
4493
|
+
}
|
|
4306
4494
|
/**
|
|
4307
4495
|
* Get the number of messages that failed to decrypt.
|
|
4308
4496
|
* Useful for detecting key mismatches, attacks, or corruption.
|
|
@@ -4379,7 +4567,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4379
4567
|
const combined = new Uint8Array(x25519Shared.length + pqShared.length);
|
|
4380
4568
|
combined.set(x25519Shared, 0);
|
|
4381
4569
|
combined.set(pqShared, x25519Shared.length);
|
|
4382
|
-
initialKey = new Uint8Array(await crypto.subtle.digest("SHA-256", combined));
|
|
4570
|
+
initialKey = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", combined));
|
|
4383
4571
|
} catch {
|
|
4384
4572
|
initialKey = x25519Shared;
|
|
4385
4573
|
}
|
|
@@ -4478,6 +4666,8 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4478
4666
|
payload.thread_id = options.threadId;
|
|
4479
4667
|
payload.reply_to = options.replyTo;
|
|
4480
4668
|
}
|
|
4669
|
+
this._persistRatchetState().catch(() => {
|
|
4670
|
+
});
|
|
4481
4671
|
const relays = [this.baseUrl, ...this.fallbackRelays];
|
|
4482
4672
|
let lastError = null;
|
|
4483
4673
|
for (const relay of relays) {
|
|
@@ -4536,8 +4726,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4536
4726
|
throw new Error(`Receive failed: ${err.error?.message || err.error || res.statusText}`);
|
|
4537
4727
|
}
|
|
4538
4728
|
const data = await res.json();
|
|
4729
|
+
return this._decryptMessages(data.messages);
|
|
4730
|
+
}
|
|
4731
|
+
/** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports) */
|
|
4732
|
+
async _decryptMessages(rawMessages) {
|
|
4539
4733
|
const decrypted = [];
|
|
4540
|
-
for (const msg of
|
|
4734
|
+
for (const msg of rawMessages) {
|
|
4541
4735
|
try {
|
|
4542
4736
|
if (this._seenMessageIds.has(msg.id)) continue;
|
|
4543
4737
|
let senderEncPub;
|
|
@@ -4561,7 +4755,6 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4561
4755
|
let envelopePqCiphertext = null;
|
|
4562
4756
|
let envelopeDhRatchetKey = null;
|
|
4563
4757
|
let envelopePn = 0;
|
|
4564
|
-
let envelopeDeniable = false;
|
|
4565
4758
|
if (msg.envelope) {
|
|
4566
4759
|
try {
|
|
4567
4760
|
const env = JSON.parse(msg.envelope);
|
|
@@ -4594,7 +4787,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4594
4787
|
const combined = new Uint8Array(x25519Shared.length + pqShared.length);
|
|
4595
4788
|
combined.set(x25519Shared, 0);
|
|
4596
4789
|
combined.set(pqShared, x25519Shared.length);
|
|
4597
|
-
initialKey = new Uint8Array(await crypto.subtle.digest("SHA-256", combined));
|
|
4790
|
+
initialKey = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", combined));
|
|
4598
4791
|
} catch {
|
|
4599
4792
|
initialKey = x25519Shared;
|
|
4600
4793
|
}
|
|
@@ -4791,6 +4984,10 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4791
4984
|
this._decryptFailCount++;
|
|
4792
4985
|
}
|
|
4793
4986
|
}
|
|
4987
|
+
if (decrypted.length > 0) {
|
|
4988
|
+
this._persistRatchetState().catch(() => {
|
|
4989
|
+
});
|
|
4990
|
+
}
|
|
4794
4991
|
return decrypted;
|
|
4795
4992
|
}
|
|
4796
4993
|
// ─── Message Management ─────────────────────────────────────────────────────
|
|
@@ -6079,6 +6276,114 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6079
6276
|
this.ping().catch(() => {
|
|
6080
6277
|
});
|
|
6081
6278
|
}
|
|
6279
|
+
const deliverMessages = async (messages) => {
|
|
6280
|
+
for (const msg of messages) {
|
|
6281
|
+
try {
|
|
6282
|
+
await onMessage(msg);
|
|
6283
|
+
if (autoMarkRead) {
|
|
6284
|
+
await this.markRead(msg.id).catch(() => {
|
|
6285
|
+
});
|
|
6286
|
+
}
|
|
6287
|
+
} catch (err) {
|
|
6288
|
+
if (onError) onError(err instanceof Error ? err : new Error(String(err)));
|
|
6289
|
+
}
|
|
6290
|
+
}
|
|
6291
|
+
if (messages.length > 0) {
|
|
6292
|
+
lastSeen = messages[messages.length - 1].timestamp;
|
|
6293
|
+
}
|
|
6294
|
+
};
|
|
6295
|
+
let lastEventId = "";
|
|
6296
|
+
let sseFailures = 0;
|
|
6297
|
+
const startSSE = async () => {
|
|
6298
|
+
try {
|
|
6299
|
+
const params = new URLSearchParams();
|
|
6300
|
+
if (lastSeen) params.set("since", lastSeen);
|
|
6301
|
+
if (options.from) params.set("from", options.from);
|
|
6302
|
+
const sseUrl = `${this.baseUrl}/v1/agent/receive/sse?${params}`;
|
|
6303
|
+
const headers = { "X-Agent-Key": this.apiKey };
|
|
6304
|
+
if (lastEventId) headers["Last-Event-ID"] = lastEventId;
|
|
6305
|
+
const controller = new AbortController();
|
|
6306
|
+
const sseTimeout = setTimeout(() => controller.abort(), 6e4);
|
|
6307
|
+
let res;
|
|
6308
|
+
try {
|
|
6309
|
+
res = await fetch(sseUrl, { headers, signal: controller.signal });
|
|
6310
|
+
} catch (e) {
|
|
6311
|
+
clearTimeout(sseTimeout);
|
|
6312
|
+
throw e;
|
|
6313
|
+
}
|
|
6314
|
+
if (!res.ok || !res.body) {
|
|
6315
|
+
clearTimeout(sseTimeout);
|
|
6316
|
+
return false;
|
|
6317
|
+
}
|
|
6318
|
+
const reader = res.body.getReader();
|
|
6319
|
+
const decoder = new TextDecoder();
|
|
6320
|
+
let buffer = "";
|
|
6321
|
+
try {
|
|
6322
|
+
while (active && !options.signal?.aborted) {
|
|
6323
|
+
const { done: streamDone, value } = await reader.read();
|
|
6324
|
+
if (streamDone) break;
|
|
6325
|
+
buffer += decoder.decode(value, { stream: true });
|
|
6326
|
+
const lines = buffer.split("\n");
|
|
6327
|
+
buffer = lines.pop() || "";
|
|
6328
|
+
let eventType = "";
|
|
6329
|
+
let dataStr = "";
|
|
6330
|
+
let eventId = "";
|
|
6331
|
+
for (const line of lines) {
|
|
6332
|
+
if (line.startsWith("event: ")) {
|
|
6333
|
+
eventType = line.slice(7).trim();
|
|
6334
|
+
} else if (line.startsWith("data: ")) {
|
|
6335
|
+
dataStr += (dataStr ? "\n" : "") + line.slice(6);
|
|
6336
|
+
} else if (line.startsWith("id: ")) {
|
|
6337
|
+
eventId = line.slice(4).trim();
|
|
6338
|
+
} else if (line === "" && (dataStr || eventType)) {
|
|
6339
|
+
if (eventId) lastEventId = eventId;
|
|
6340
|
+
if (eventType === "message" && dataStr) {
|
|
6341
|
+
try {
|
|
6342
|
+
const rawMsg = JSON.parse(dataStr);
|
|
6343
|
+
const decrypted = await this._decryptMessages([rawMsg]);
|
|
6344
|
+
if (decrypted.length > 0) {
|
|
6345
|
+
consecutiveEmpty = 0;
|
|
6346
|
+
sseFailures = 0;
|
|
6347
|
+
await deliverMessages(decrypted);
|
|
6348
|
+
}
|
|
6349
|
+
} catch {
|
|
6350
|
+
}
|
|
6351
|
+
} else if (eventType === "reconnect") {
|
|
6352
|
+
break;
|
|
6353
|
+
}
|
|
6354
|
+
eventType = "";
|
|
6355
|
+
dataStr = "";
|
|
6356
|
+
eventId = "";
|
|
6357
|
+
}
|
|
6358
|
+
}
|
|
6359
|
+
}
|
|
6360
|
+
} finally {
|
|
6361
|
+
reader.releaseLock();
|
|
6362
|
+
clearTimeout(sseTimeout);
|
|
6363
|
+
}
|
|
6364
|
+
sseFailures = 0;
|
|
6365
|
+
return true;
|
|
6366
|
+
} catch {
|
|
6367
|
+
sseFailures++;
|
|
6368
|
+
return false;
|
|
6369
|
+
}
|
|
6370
|
+
};
|
|
6371
|
+
const sseLoop = async () => {
|
|
6372
|
+
while (active && !options.signal?.aborted) {
|
|
6373
|
+
const ok = await startSSE();
|
|
6374
|
+
if (!active || options.signal?.aborted) break;
|
|
6375
|
+
if (!ok) {
|
|
6376
|
+
if (sseFailures >= 3) {
|
|
6377
|
+
poll();
|
|
6378
|
+
return;
|
|
6379
|
+
}
|
|
6380
|
+
const backoff = Math.min(1e3 * Math.pow(2, sseFailures - 1), 4e3);
|
|
6381
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
6382
|
+
continue;
|
|
6383
|
+
}
|
|
6384
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
6385
|
+
}
|
|
6386
|
+
};
|
|
6082
6387
|
const useLongPoll = this.longPoll;
|
|
6083
6388
|
const poll = async () => {
|
|
6084
6389
|
if (!active || options.signal?.aborted) {
|
|
@@ -6086,62 +6391,18 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6086
6391
|
return;
|
|
6087
6392
|
}
|
|
6088
6393
|
try {
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
if (res.ok) {
|
|
6098
|
-
const data = await res.json();
|
|
6099
|
-
messages = [];
|
|
6100
|
-
for (const raw of data.messages) {
|
|
6101
|
-
try {
|
|
6102
|
-
if (this._seenMessageIds.has(raw.id)) continue;
|
|
6103
|
-
this._seenMessageIds.add(raw.id);
|
|
6104
|
-
} catch {
|
|
6105
|
-
}
|
|
6106
|
-
}
|
|
6107
|
-
if (data.messages.length > 0) {
|
|
6108
|
-
messages = await this.receive({
|
|
6109
|
-
since: lastSeen,
|
|
6110
|
-
from: options.from,
|
|
6111
|
-
threadId: options.threadId,
|
|
6112
|
-
messageType: options.messageType,
|
|
6113
|
-
unreadOnly,
|
|
6114
|
-
limit: 50
|
|
6115
|
-
});
|
|
6116
|
-
}
|
|
6117
|
-
} else {
|
|
6118
|
-
messages = [];
|
|
6119
|
-
}
|
|
6120
|
-
} else {
|
|
6121
|
-
messages = await this.receive({
|
|
6122
|
-
since: lastSeen,
|
|
6123
|
-
from: options.from,
|
|
6124
|
-
threadId: options.threadId,
|
|
6125
|
-
messageType: options.messageType,
|
|
6126
|
-
unreadOnly,
|
|
6127
|
-
limit: 50
|
|
6128
|
-
});
|
|
6129
|
-
}
|
|
6394
|
+
const messages = await this.receive({
|
|
6395
|
+
since: lastSeen,
|
|
6396
|
+
from: options.from,
|
|
6397
|
+
threadId: options.threadId,
|
|
6398
|
+
messageType: options.messageType,
|
|
6399
|
+
unreadOnly,
|
|
6400
|
+
limit: 50
|
|
6401
|
+
});
|
|
6130
6402
|
if (messages.length > 0) {
|
|
6131
6403
|
consecutiveEmpty = 0;
|
|
6132
6404
|
if (adaptive) currentInterval = Math.max(interval / 2, 500);
|
|
6133
|
-
|
|
6134
|
-
try {
|
|
6135
|
-
await onMessage(msg);
|
|
6136
|
-
if (autoMarkRead) {
|
|
6137
|
-
await this.markRead(msg.id).catch(() => {
|
|
6138
|
-
});
|
|
6139
|
-
}
|
|
6140
|
-
} catch (err) {
|
|
6141
|
-
if (onError) onError(err instanceof Error ? err : new Error(String(err)));
|
|
6142
|
-
}
|
|
6143
|
-
}
|
|
6144
|
-
lastSeen = messages[messages.length - 1].timestamp;
|
|
6405
|
+
await deliverMessages(messages);
|
|
6145
6406
|
} else {
|
|
6146
6407
|
consecutiveEmpty++;
|
|
6147
6408
|
if (adaptive && consecutiveEmpty > 3 && !useLongPoll) {
|
|
@@ -6156,7 +6417,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6156
6417
|
timer = setTimeout(poll, useLongPoll ? 100 : currentInterval);
|
|
6157
6418
|
}
|
|
6158
6419
|
};
|
|
6159
|
-
|
|
6420
|
+
const prefs = this._transportPrefs;
|
|
6421
|
+
if (prefs.includes("sse")) {
|
|
6422
|
+
sseLoop();
|
|
6423
|
+
} else {
|
|
6424
|
+
poll();
|
|
6425
|
+
}
|
|
6160
6426
|
return handle;
|
|
6161
6427
|
}
|
|
6162
6428
|
/**
|
|
@@ -6488,7 +6754,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6488
6754
|
for (const relay of relays) {
|
|
6489
6755
|
try {
|
|
6490
6756
|
const res = await this._timedFetch(`${relay}${path}`, init);
|
|
6491
|
-
if (res.ok || res.status >= 400 && res.status < 500) return res;
|
|
6757
|
+
if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) return res;
|
|
6758
|
+
if (res.status === 429) {
|
|
6759
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
6760
|
+
const waitMs = retryAfter ? Math.min(parseInt(retryAfter, 10) * 1e3, 5e3) : 1e3;
|
|
6761
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
6762
|
+
}
|
|
6492
6763
|
} catch (err) {
|
|
6493
6764
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
6494
6765
|
}
|
|
@@ -6675,7 +6946,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6675
6946
|
...this.requireSignatures ? ["Strict signature enforcement (reject unsigned/invalid messages)"] : [],
|
|
6676
6947
|
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays \u2014 receive, discover, identity all use fallbacks)`] : [],
|
|
6677
6948
|
...this.jitterMs > 0 ? [`Timing jitter (random ${this.jitterMs}ms delay \u2014 metadata timing protection)`] : [],
|
|
6949
|
+
...this._transportPrefs.includes("sse") ? ["SSE streaming transport (real-time push delivery from relay)"] : [],
|
|
6678
6950
|
...this.longPoll ? ["Long-poll transport (25s server-held connection \u2014 near-real-time delivery)"] : [],
|
|
6951
|
+
...this._persistMode !== "memory" ? [`Ratchet state auto-persistence (${this._persistMode} backend \u2014 survives process restart)`] : [],
|
|
6679
6952
|
...this._coverTrafficTimer !== null ? ["Cover traffic (encrypted noise at random intervals \u2014 traffic analysis resistance)"] : [],
|
|
6680
6953
|
"Agent RPC (invoke/onInvoke \u2014 synchronous function calls between agents)",
|
|
6681
6954
|
"P2P direct send (bypass relay via webhook \u2014 true peer-to-peer when possible)",
|
|
@@ -6689,7 +6962,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6689
6962
|
...this.sealedSender ? ["Relay sees to_did (needed for routing) but NOT from_did, thread_id, or message_type"] : ["Relay sees from_did, to_did, thread_id, message_type in cleartext \u2014 enable sealedSender to strip metadata"],
|
|
6690
6963
|
"Relay sees channel membership, task delegation, trust scores (social graph)",
|
|
6691
6964
|
...this.fallbackRelays.length === 0 ? ["Single relay with no fallbacks \u2014 configure fallbackRelays for resilience"] : [],
|
|
6692
|
-
"Ratchet state is in-memory (lost on process restart \u2014
|
|
6965
|
+
...this._persistMode === "memory" ? ["Ratchet state is in-memory (lost on process restart \u2014 use persist option or exportCredentials)"] : [],
|
|
6693
6966
|
...!this.deniable ? ["Ed25519 signatures are non-repudiable \u2014 enable deniable option for HMAC auth"] : [],
|
|
6694
6967
|
...!this.doubleRatchet ? ["Hash ratchet only \u2014 enable doubleRatchet option for post-compromise recovery"] : [],
|
|
6695
6968
|
...this.jitterMs === 0 ? ["No timing jitter \u2014 enable jitterMs option for metadata protection"] : [],
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidly/agent-sdk",
|
|
3
|
-
"version": "3.1
|
|
4
|
-
"description": "E2E encrypted agent-to-agent communication SDK — Double Ratchet, X3DH, deniable auth, ML-KEM-768 post-quantum,
|
|
3
|
+
"version": "3.2.1",
|
|
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",
|
|
7
7
|
"types": "dist/index.d.ts",
|