@voidly/agent-sdk 3.0.0 → 3.2.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.mts +137 -0
- package/dist/index.d.ts +137 -0
- package/dist/index.js +663 -88
- package/dist/index.mjs +663 -88
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3983,19 +3983,29 @@ async function ratchetStep(chainKey) {
|
|
|
3983
3983
|
);
|
|
3984
3984
|
return { nextChainKey, messageKey };
|
|
3985
3985
|
}
|
|
3986
|
-
function sealEnvelope(senderDid, plaintext) {
|
|
3987
|
-
|
|
3988
|
-
v:
|
|
3986
|
+
function sealEnvelope(senderDid, plaintext, meta) {
|
|
3987
|
+
const obj = {
|
|
3988
|
+
v: 3,
|
|
3989
3989
|
from: senderDid,
|
|
3990
3990
|
msg: plaintext,
|
|
3991
3991
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
3992
|
-
}
|
|
3992
|
+
};
|
|
3993
|
+
if (meta?.contentType && meta.contentType !== "text/plain") obj.ct = meta.contentType;
|
|
3994
|
+
if (meta?.messageType && meta.messageType !== "text") obj.mt = meta.messageType;
|
|
3995
|
+
if (meta?.threadId) obj.tid = meta.threadId;
|
|
3996
|
+
if (meta?.replyTo) obj.rto = meta.replyTo;
|
|
3997
|
+
return JSON.stringify(obj);
|
|
3993
3998
|
}
|
|
3994
3999
|
function unsealEnvelope(plaintext) {
|
|
3995
4000
|
try {
|
|
3996
4001
|
const parsed = JSON.parse(plaintext);
|
|
3997
|
-
if (parsed.v === 2 && parsed.from && parsed.msg) {
|
|
3998
|
-
|
|
4002
|
+
if ((parsed.v === 2 || parsed.v === 3) && parsed.from && parsed.msg) {
|
|
4003
|
+
const result = { from: parsed.from, msg: parsed.msg, ts: parsed.ts };
|
|
4004
|
+
if (parsed.ct) result.contentType = parsed.ct;
|
|
4005
|
+
if (parsed.mt) result.messageType = parsed.mt;
|
|
4006
|
+
if (parsed.tid) result.threadId = parsed.tid;
|
|
4007
|
+
if (parsed.rto) result.replyTo = parsed.rto;
|
|
4008
|
+
return result;
|
|
3999
4009
|
}
|
|
4000
4010
|
return null;
|
|
4001
4011
|
} catch {
|
|
@@ -4073,6 +4083,17 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4073
4083
|
this._identityCache = /* @__PURE__ */ new Map();
|
|
4074
4084
|
this._seenMessageIds = /* @__PURE__ */ new Set();
|
|
4075
4085
|
this._decryptFailCount = 0;
|
|
4086
|
+
// RPC handlers: method → handler function
|
|
4087
|
+
this._rpcHandlers = /* @__PURE__ */ new Map();
|
|
4088
|
+
// RPC pending responses: rpc_id → { resolve, reject, timer }
|
|
4089
|
+
this._rpcPending = /* @__PURE__ */ new Map();
|
|
4090
|
+
// Cover traffic state
|
|
4091
|
+
this._coverTrafficTimer = null;
|
|
4092
|
+
// RPC listener handle (started on first onInvoke)
|
|
4093
|
+
this._rpcListener = null;
|
|
4094
|
+
// Persistence (v3.2)
|
|
4095
|
+
this._persistMode = "memory";
|
|
4096
|
+
this._persistKey = null;
|
|
4076
4097
|
this.did = identity.did;
|
|
4077
4098
|
this.apiKey = identity.apiKey;
|
|
4078
4099
|
this.signingKeyPair = identity.signingKeyPair;
|
|
@@ -4092,6 +4113,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4092
4113
|
this.longPoll = config?.longPoll !== false;
|
|
4093
4114
|
this.mlkemPublicKey = identity.mlkemPublicKey || null;
|
|
4094
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"];
|
|
4095
4121
|
}
|
|
4096
4122
|
// ─── Factory Methods ────────────────────────────────────────────────────────
|
|
4097
4123
|
/**
|
|
@@ -4296,6 +4322,186 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4296
4322
|
} : {}
|
|
4297
4323
|
};
|
|
4298
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
|
+
}
|
|
4299
4505
|
/**
|
|
4300
4506
|
* Get the number of messages that failed to decrypt.
|
|
4301
4507
|
* Useful for detecting key mismatches, attacks, or corruption.
|
|
@@ -4343,7 +4549,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4343
4549
|
const recipientPubKey = (0, import_tweetnacl_util.decodeBase64)(profile.encryption_public_key);
|
|
4344
4550
|
let plaintext = message;
|
|
4345
4551
|
if (useSealed) {
|
|
4346
|
-
plaintext = sealEnvelope(this.did, message
|
|
4552
|
+
plaintext = sealEnvelope(this.did, message, {
|
|
4553
|
+
contentType: options.contentType,
|
|
4554
|
+
messageType: options.messageType,
|
|
4555
|
+
threadId: options.threadId,
|
|
4556
|
+
replyTo: options.replyTo
|
|
4557
|
+
});
|
|
4347
4558
|
}
|
|
4348
4559
|
let contentBytes;
|
|
4349
4560
|
if (usePadding) {
|
|
@@ -4458,12 +4669,16 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4458
4669
|
nonce: (0, import_tweetnacl_util.encodeBase64)(nonce),
|
|
4459
4670
|
signature: (0, import_tweetnacl_util.encodeBase64)(signature),
|
|
4460
4671
|
envelope: envelopeData,
|
|
4461
|
-
content_type: options.contentType || "text/plain",
|
|
4462
|
-
message_type: options.messageType || "text",
|
|
4463
|
-
thread_id: options.threadId,
|
|
4464
|
-
reply_to: options.replyTo,
|
|
4465
4672
|
ttl: options.ttl
|
|
4466
4673
|
};
|
|
4674
|
+
if (!useSealed) {
|
|
4675
|
+
payload.content_type = options.contentType || "text/plain";
|
|
4676
|
+
payload.message_type = options.messageType || "text";
|
|
4677
|
+
payload.thread_id = options.threadId;
|
|
4678
|
+
payload.reply_to = options.replyTo;
|
|
4679
|
+
}
|
|
4680
|
+
this._persistRatchetState().catch(() => {
|
|
4681
|
+
});
|
|
4467
4682
|
const relays = [this.baseUrl, ...this.fallbackRelays];
|
|
4468
4683
|
let lastError = null;
|
|
4469
4684
|
for (const relay of relays) {
|
|
@@ -4514,7 +4729,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4514
4729
|
if (options.contentType) params.set("content_type", options.contentType);
|
|
4515
4730
|
if (options.messageType) params.set("message_type", options.messageType);
|
|
4516
4731
|
if (options.unreadOnly) params.set("unread", "true");
|
|
4517
|
-
const res = await this.
|
|
4732
|
+
const res = await this._resilientFetch(`/v1/agent/receive/raw?${params}`, {
|
|
4518
4733
|
headers: { "X-Agent-Key": this.apiKey }
|
|
4519
4734
|
});
|
|
4520
4735
|
if (!res.ok) {
|
|
@@ -4522,11 +4737,28 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4522
4737
|
throw new Error(`Receive failed: ${err.error?.message || err.error || res.statusText}`);
|
|
4523
4738
|
}
|
|
4524
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) {
|
|
4525
4744
|
const decrypted = [];
|
|
4526
|
-
for (const msg of
|
|
4745
|
+
for (const msg of rawMessages) {
|
|
4527
4746
|
try {
|
|
4528
4747
|
if (this._seenMessageIds.has(msg.id)) continue;
|
|
4529
|
-
|
|
4748
|
+
let senderEncPub;
|
|
4749
|
+
let senderSignPubBytes = null;
|
|
4750
|
+
if (msg.sender_encryption_key) {
|
|
4751
|
+
senderEncPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_encryption_key);
|
|
4752
|
+
if (msg.sender_signing_key) senderSignPubBytes = (0, import_tweetnacl_util.decodeBase64)(msg.sender_signing_key);
|
|
4753
|
+
} else if (msg.envelope) {
|
|
4754
|
+
const env = JSON.parse(msg.envelope);
|
|
4755
|
+
const senderProfile = await this.getIdentity(env.from);
|
|
4756
|
+
if (!senderProfile) continue;
|
|
4757
|
+
senderEncPub = (0, import_tweetnacl_util.decodeBase64)(senderProfile.encryption_public_key);
|
|
4758
|
+
if (senderProfile.signing_public_key) senderSignPubBytes = (0, import_tweetnacl_util.decodeBase64)(senderProfile.signing_public_key);
|
|
4759
|
+
} else {
|
|
4760
|
+
continue;
|
|
4761
|
+
}
|
|
4530
4762
|
const ciphertext = (0, import_tweetnacl_util.decodeBase64)(msg.ciphertext);
|
|
4531
4763
|
const nonce = (0, import_tweetnacl_util.decodeBase64)(msg.nonce);
|
|
4532
4764
|
let rawPlaintext = null;
|
|
@@ -4534,7 +4766,6 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4534
4766
|
let envelopePqCiphertext = null;
|
|
4535
4767
|
let envelopeDhRatchetKey = null;
|
|
4536
4768
|
let envelopePn = 0;
|
|
4537
|
-
let envelopeDeniable = false;
|
|
4538
4769
|
if (msg.envelope) {
|
|
4539
4770
|
try {
|
|
4540
4771
|
const env = JSON.parse(msg.envelope);
|
|
@@ -4694,11 +4925,19 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4694
4925
|
}
|
|
4695
4926
|
let content = (0, import_tweetnacl_util.encodeUTF8)(plaintextBytes);
|
|
4696
4927
|
let senderDid = msg.from;
|
|
4928
|
+
let innerContentType;
|
|
4929
|
+
let innerMessageType;
|
|
4930
|
+
let innerThreadId;
|
|
4931
|
+
let innerReplyTo;
|
|
4697
4932
|
if (wasSealed || !proto) {
|
|
4698
4933
|
const unsealed = unsealEnvelope(content);
|
|
4699
4934
|
if (unsealed) {
|
|
4700
4935
|
content = unsealed.msg;
|
|
4701
4936
|
senderDid = unsealed.from;
|
|
4937
|
+
innerContentType = unsealed.contentType;
|
|
4938
|
+
innerMessageType = unsealed.messageType;
|
|
4939
|
+
innerThreadId = unsealed.threadId;
|
|
4940
|
+
innerReplyTo = unsealed.replyTo;
|
|
4702
4941
|
}
|
|
4703
4942
|
}
|
|
4704
4943
|
let signatureValid = false;
|
|
@@ -4719,12 +4958,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4719
4958
|
for (let i = 0; i < expectedHmac.length; i++) diff |= expectedHmac[i] ^ signatureBytes[i];
|
|
4720
4959
|
signatureValid = diff === 0;
|
|
4721
4960
|
}
|
|
4722
|
-
} else {
|
|
4723
|
-
const senderSignPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_signing_key);
|
|
4961
|
+
} else if (senderSignPubBytes) {
|
|
4724
4962
|
signatureValid = import_tweetnacl.default.sign.detached.verify(
|
|
4725
4963
|
(0, import_tweetnacl_util.decodeUTF8)(envelopeStr),
|
|
4726
4964
|
signatureBytes,
|
|
4727
|
-
|
|
4965
|
+
senderSignPubBytes
|
|
4728
4966
|
);
|
|
4729
4967
|
}
|
|
4730
4968
|
} catch {
|
|
@@ -4744,10 +4982,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4744
4982
|
from: senderDid,
|
|
4745
4983
|
to: msg.to,
|
|
4746
4984
|
content,
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4985
|
+
// v3: prefer metadata from inside ciphertext (relay can't see it)
|
|
4986
|
+
contentType: innerContentType || msg.content_type || "text/plain",
|
|
4987
|
+
messageType: innerMessageType || msg.message_type || "text",
|
|
4988
|
+
threadId: innerThreadId || msg.thread_id || null,
|
|
4989
|
+
replyTo: innerReplyTo || msg.reply_to || null,
|
|
4751
4990
|
signatureValid,
|
|
4752
4991
|
timestamp: msg.timestamp,
|
|
4753
4992
|
expiresAt: msg.expires_at
|
|
@@ -4756,6 +4995,10 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4756
4995
|
this._decryptFailCount++;
|
|
4757
4996
|
}
|
|
4758
4997
|
}
|
|
4998
|
+
if (decrypted.length > 0) {
|
|
4999
|
+
this._persistRatchetState().catch(() => {
|
|
5000
|
+
});
|
|
5001
|
+
}
|
|
4759
5002
|
return decrypted;
|
|
4760
5003
|
}
|
|
4761
5004
|
// ─── Message Management ─────────────────────────────────────────────────────
|
|
@@ -4808,7 +5051,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4808
5051
|
if (cached && Date.now() - cached.cachedAt < 3e5) {
|
|
4809
5052
|
return cached.profile;
|
|
4810
5053
|
}
|
|
4811
|
-
const res = await this.
|
|
5054
|
+
const res = await this._resilientFetch(`/v1/agent/identity/${did}`);
|
|
4812
5055
|
if (!res.ok) return null;
|
|
4813
5056
|
const profile = await res.json();
|
|
4814
5057
|
this._identityCache.set(did, { profile, cachedAt: Date.now() });
|
|
@@ -4826,7 +5069,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4826
5069
|
if (options.query) params.set("query", options.query);
|
|
4827
5070
|
if (options.capability) params.set("capability", options.capability);
|
|
4828
5071
|
if (options.limit) params.set("limit", String(options.limit));
|
|
4829
|
-
const res = await this.
|
|
5072
|
+
const res = await this._resilientFetch(`/v1/agent/discover?${params}`);
|
|
4830
5073
|
if (!res.ok) return [];
|
|
4831
5074
|
const data = await res.json();
|
|
4832
5075
|
return data.agents;
|
|
@@ -6044,6 +6287,84 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6044
6287
|
this.ping().catch(() => {
|
|
6045
6288
|
});
|
|
6046
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
|
+
const startSSE = async () => {
|
|
6307
|
+
try {
|
|
6308
|
+
const params = new URLSearchParams();
|
|
6309
|
+
if (lastSeen) params.set("since", lastSeen);
|
|
6310
|
+
if (options.from) params.set("from", options.from);
|
|
6311
|
+
const sseUrl = `${this.baseUrl}/v1/agent/receive/sse?${params}`;
|
|
6312
|
+
const res = await this._timedFetch(sseUrl, {
|
|
6313
|
+
headers: { "X-Agent-Key": this.apiKey }
|
|
6314
|
+
});
|
|
6315
|
+
if (!res.ok || !res.body) return false;
|
|
6316
|
+
const reader = res.body.getReader();
|
|
6317
|
+
const decoder = new TextDecoder();
|
|
6318
|
+
let buffer = "";
|
|
6319
|
+
while (active && !options.signal?.aborted) {
|
|
6320
|
+
const { done: streamDone, value } = await reader.read();
|
|
6321
|
+
if (streamDone) break;
|
|
6322
|
+
buffer += decoder.decode(value, { stream: true });
|
|
6323
|
+
const lines = buffer.split("\n");
|
|
6324
|
+
buffer = lines.pop() || "";
|
|
6325
|
+
let eventType = "";
|
|
6326
|
+
let dataStr = "";
|
|
6327
|
+
for (const line of lines) {
|
|
6328
|
+
if (line.startsWith("event: ")) {
|
|
6329
|
+
eventType = line.slice(7).trim();
|
|
6330
|
+
} else if (line.startsWith("data: ")) {
|
|
6331
|
+
dataStr = line.slice(6);
|
|
6332
|
+
} else if (line === "" && dataStr) {
|
|
6333
|
+
if (eventType === "message" && dataStr) {
|
|
6334
|
+
try {
|
|
6335
|
+
const rawMsg = JSON.parse(dataStr);
|
|
6336
|
+
const decrypted = await this._decryptMessages([rawMsg]);
|
|
6337
|
+
if (decrypted.length > 0) {
|
|
6338
|
+
consecutiveEmpty = 0;
|
|
6339
|
+
await deliverMessages(decrypted);
|
|
6340
|
+
}
|
|
6341
|
+
} catch {
|
|
6342
|
+
}
|
|
6343
|
+
} else if (eventType === "reconnect") {
|
|
6344
|
+
break;
|
|
6345
|
+
}
|
|
6346
|
+
eventType = "";
|
|
6347
|
+
dataStr = "";
|
|
6348
|
+
}
|
|
6349
|
+
}
|
|
6350
|
+
}
|
|
6351
|
+
reader.releaseLock();
|
|
6352
|
+
return true;
|
|
6353
|
+
} catch {
|
|
6354
|
+
return false;
|
|
6355
|
+
}
|
|
6356
|
+
};
|
|
6357
|
+
const sseLoop = async () => {
|
|
6358
|
+
while (active && !options.signal?.aborted) {
|
|
6359
|
+
const ok = await startSSE();
|
|
6360
|
+
if (!active || options.signal?.aborted) break;
|
|
6361
|
+
if (!ok) {
|
|
6362
|
+
poll();
|
|
6363
|
+
return;
|
|
6364
|
+
}
|
|
6365
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
6366
|
+
}
|
|
6367
|
+
};
|
|
6047
6368
|
const useLongPoll = this.longPoll;
|
|
6048
6369
|
const poll = async () => {
|
|
6049
6370
|
if (!active || options.signal?.aborted) {
|
|
@@ -6051,62 +6372,18 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6051
6372
|
return;
|
|
6052
6373
|
}
|
|
6053
6374
|
try {
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
if (res.ok) {
|
|
6063
|
-
const data = await res.json();
|
|
6064
|
-
messages = [];
|
|
6065
|
-
for (const raw of data.messages) {
|
|
6066
|
-
try {
|
|
6067
|
-
if (this._seenMessageIds.has(raw.id)) continue;
|
|
6068
|
-
this._seenMessageIds.add(raw.id);
|
|
6069
|
-
} catch {
|
|
6070
|
-
}
|
|
6071
|
-
}
|
|
6072
|
-
if (data.messages.length > 0) {
|
|
6073
|
-
messages = await this.receive({
|
|
6074
|
-
since: lastSeen,
|
|
6075
|
-
from: options.from,
|
|
6076
|
-
threadId: options.threadId,
|
|
6077
|
-
messageType: options.messageType,
|
|
6078
|
-
unreadOnly,
|
|
6079
|
-
limit: 50
|
|
6080
|
-
});
|
|
6081
|
-
}
|
|
6082
|
-
} else {
|
|
6083
|
-
messages = [];
|
|
6084
|
-
}
|
|
6085
|
-
} else {
|
|
6086
|
-
messages = await this.receive({
|
|
6087
|
-
since: lastSeen,
|
|
6088
|
-
from: options.from,
|
|
6089
|
-
threadId: options.threadId,
|
|
6090
|
-
messageType: options.messageType,
|
|
6091
|
-
unreadOnly,
|
|
6092
|
-
limit: 50
|
|
6093
|
-
});
|
|
6094
|
-
}
|
|
6375
|
+
const messages = await this.receive({
|
|
6376
|
+
since: lastSeen,
|
|
6377
|
+
from: options.from,
|
|
6378
|
+
threadId: options.threadId,
|
|
6379
|
+
messageType: options.messageType,
|
|
6380
|
+
unreadOnly,
|
|
6381
|
+
limit: 50
|
|
6382
|
+
});
|
|
6095
6383
|
if (messages.length > 0) {
|
|
6096
6384
|
consecutiveEmpty = 0;
|
|
6097
6385
|
if (adaptive) currentInterval = Math.max(interval / 2, 500);
|
|
6098
|
-
|
|
6099
|
-
try {
|
|
6100
|
-
await onMessage(msg);
|
|
6101
|
-
if (autoMarkRead) {
|
|
6102
|
-
await this.markRead(msg.id).catch(() => {
|
|
6103
|
-
});
|
|
6104
|
-
}
|
|
6105
|
-
} catch (err) {
|
|
6106
|
-
if (onError) onError(err instanceof Error ? err : new Error(String(err)));
|
|
6107
|
-
}
|
|
6108
|
-
}
|
|
6109
|
-
lastSeen = messages[messages.length - 1].timestamp;
|
|
6386
|
+
await deliverMessages(messages);
|
|
6110
6387
|
} else {
|
|
6111
6388
|
consecutiveEmpty++;
|
|
6112
6389
|
if (adaptive && consecutiveEmpty > 3 && !useLongPoll) {
|
|
@@ -6121,7 +6398,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6121
6398
|
timer = setTimeout(poll, useLongPoll ? 100 : currentInterval);
|
|
6122
6399
|
}
|
|
6123
6400
|
};
|
|
6124
|
-
|
|
6401
|
+
const prefs = this._transportPrefs;
|
|
6402
|
+
if (prefs.includes("sse")) {
|
|
6403
|
+
sseLoop();
|
|
6404
|
+
} else {
|
|
6405
|
+
poll();
|
|
6406
|
+
}
|
|
6125
6407
|
return handle;
|
|
6126
6408
|
}
|
|
6127
6409
|
/**
|
|
@@ -6180,6 +6462,285 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6180
6462
|
listener.stop();
|
|
6181
6463
|
}
|
|
6182
6464
|
this._listeners.clear();
|
|
6465
|
+
if (this._rpcListener) {
|
|
6466
|
+
this._rpcListener.stop();
|
|
6467
|
+
this._rpcListener = null;
|
|
6468
|
+
}
|
|
6469
|
+
for (const [id, pending] of this._rpcPending) {
|
|
6470
|
+
clearTimeout(pending.timer);
|
|
6471
|
+
pending.reject(new Error("Agent stopped"));
|
|
6472
|
+
}
|
|
6473
|
+
this._rpcPending.clear();
|
|
6474
|
+
this.disableCoverTraffic();
|
|
6475
|
+
}
|
|
6476
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6477
|
+
// AGENT RPC — Synchronous Function Invocation Between Agents
|
|
6478
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6479
|
+
/**
|
|
6480
|
+
* Invoke a function on a remote agent. Synchronous RPC over encrypted messaging.
|
|
6481
|
+
* The remote agent must have registered a handler via `onInvoke()`.
|
|
6482
|
+
*
|
|
6483
|
+
* @example
|
|
6484
|
+
* ```ts
|
|
6485
|
+
* // Call a translator agent
|
|
6486
|
+
* const result = await agent.invoke('did:voidly:translator', 'translate', {
|
|
6487
|
+
* text: 'Hello, world!',
|
|
6488
|
+
* to: 'ja',
|
|
6489
|
+
* });
|
|
6490
|
+
* console.log(result.translation); // こんにちは
|
|
6491
|
+
*
|
|
6492
|
+
* // With timeout
|
|
6493
|
+
* const data = await agent.invoke(peerDid, 'analyze', { url: '...' }, 15000);
|
|
6494
|
+
* ```
|
|
6495
|
+
*/
|
|
6496
|
+
async invoke(targetDid, method, params = {}, timeoutMs = 3e4) {
|
|
6497
|
+
const rpcId = `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
6498
|
+
return new Promise(async (resolve, reject) => {
|
|
6499
|
+
const timer = setTimeout(() => {
|
|
6500
|
+
this._rpcPending.delete(rpcId);
|
|
6501
|
+
reject(new Error(`RPC timeout: ${method}@${targetDid} after ${timeoutMs}ms`));
|
|
6502
|
+
}, timeoutMs);
|
|
6503
|
+
this._rpcPending.set(rpcId, { resolve, reject, timer });
|
|
6504
|
+
try {
|
|
6505
|
+
await this.send(targetDid, JSON.stringify({
|
|
6506
|
+
jsonrpc: "2.0",
|
|
6507
|
+
method,
|
|
6508
|
+
params,
|
|
6509
|
+
id: rpcId
|
|
6510
|
+
}), { messageType: "rpc-request", threadId: rpcId });
|
|
6511
|
+
} catch (err) {
|
|
6512
|
+
clearTimeout(timer);
|
|
6513
|
+
this._rpcPending.delete(rpcId);
|
|
6514
|
+
reject(err);
|
|
6515
|
+
}
|
|
6516
|
+
});
|
|
6517
|
+
}
|
|
6518
|
+
/**
|
|
6519
|
+
* Register a handler for incoming RPC invocations.
|
|
6520
|
+
* When another agent calls `invoke(yourDid, method, params)`, your handler runs.
|
|
6521
|
+
*
|
|
6522
|
+
* @example
|
|
6523
|
+
* ```ts
|
|
6524
|
+
* // Register a translation capability
|
|
6525
|
+
* agent.onInvoke('translate', async (params, callerDid) => {
|
|
6526
|
+
* const result = await myTranslateFunction(params.text, params.to);
|
|
6527
|
+
* return { translation: result };
|
|
6528
|
+
* });
|
|
6529
|
+
*
|
|
6530
|
+
* // Register a search capability
|
|
6531
|
+
* agent.onInvoke('search', async (params) => {
|
|
6532
|
+
* return { results: await searchDatabase(params.query) };
|
|
6533
|
+
* });
|
|
6534
|
+
* ```
|
|
6535
|
+
*/
|
|
6536
|
+
onInvoke(method, handler) {
|
|
6537
|
+
this._rpcHandlers.set(method, handler);
|
|
6538
|
+
this._ensureRpcListener();
|
|
6539
|
+
}
|
|
6540
|
+
/**
|
|
6541
|
+
* Remove an RPC handler.
|
|
6542
|
+
*/
|
|
6543
|
+
offInvoke(method) {
|
|
6544
|
+
this._rpcHandlers.delete(method);
|
|
6545
|
+
if (this._rpcHandlers.size === 0 && this._rpcListener) {
|
|
6546
|
+
this._rpcListener.stop();
|
|
6547
|
+
this._rpcListener = null;
|
|
6548
|
+
}
|
|
6549
|
+
}
|
|
6550
|
+
/** @internal Start listening for RPC requests and responses */
|
|
6551
|
+
_ensureRpcListener() {
|
|
6552
|
+
if (this._rpcListener) return;
|
|
6553
|
+
this._rpcListener = this.listen(async (msg) => {
|
|
6554
|
+
try {
|
|
6555
|
+
const payload = JSON.parse(msg.content);
|
|
6556
|
+
if (payload.jsonrpc !== "2.0") return;
|
|
6557
|
+
if (payload.id && (payload.result !== void 0 || payload.error)) {
|
|
6558
|
+
const pending = this._rpcPending.get(payload.id);
|
|
6559
|
+
if (pending) {
|
|
6560
|
+
clearTimeout(pending.timer);
|
|
6561
|
+
this._rpcPending.delete(payload.id);
|
|
6562
|
+
if (payload.error) {
|
|
6563
|
+
pending.reject(new Error(payload.error.message || "RPC error"));
|
|
6564
|
+
} else {
|
|
6565
|
+
pending.resolve(payload.result);
|
|
6566
|
+
}
|
|
6567
|
+
}
|
|
6568
|
+
return;
|
|
6569
|
+
}
|
|
6570
|
+
if (payload.method && payload.id) {
|
|
6571
|
+
const handler = this._rpcHandlers.get(payload.method);
|
|
6572
|
+
if (!handler) {
|
|
6573
|
+
await this.send(msg.from, JSON.stringify({
|
|
6574
|
+
jsonrpc: "2.0",
|
|
6575
|
+
id: payload.id,
|
|
6576
|
+
error: { code: -32601, message: `Method not found: ${payload.method}` }
|
|
6577
|
+
}), { messageType: "rpc-response", threadId: payload.id });
|
|
6578
|
+
return;
|
|
6579
|
+
}
|
|
6580
|
+
try {
|
|
6581
|
+
const result = await handler(payload.params || {}, msg.from);
|
|
6582
|
+
await this.send(msg.from, JSON.stringify({
|
|
6583
|
+
jsonrpc: "2.0",
|
|
6584
|
+
id: payload.id,
|
|
6585
|
+
result
|
|
6586
|
+
}), { messageType: "rpc-response", threadId: payload.id });
|
|
6587
|
+
} catch (err) {
|
|
6588
|
+
await this.send(msg.from, JSON.stringify({
|
|
6589
|
+
jsonrpc: "2.0",
|
|
6590
|
+
id: payload.id,
|
|
6591
|
+
error: { code: -32e3, message: err.message || "Handler error" }
|
|
6592
|
+
}), { messageType: "rpc-response", threadId: payload.id });
|
|
6593
|
+
}
|
|
6594
|
+
}
|
|
6595
|
+
} catch {
|
|
6596
|
+
}
|
|
6597
|
+
}, { interval: 500, adaptive: false, heartbeat: false });
|
|
6598
|
+
}
|
|
6599
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6600
|
+
// P2P DIRECT MODE — Bypass Relay When Possible
|
|
6601
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6602
|
+
/**
|
|
6603
|
+
* Send a message directly to a peer's webhook endpoint, bypassing the relay entirely.
|
|
6604
|
+
* The relay never sees the message — true peer-to-peer encrypted delivery.
|
|
6605
|
+
*
|
|
6606
|
+
* Falls back to relay-based send if direct delivery fails.
|
|
6607
|
+
*
|
|
6608
|
+
* @example
|
|
6609
|
+
* ```ts
|
|
6610
|
+
* // Try direct first, fall back to relay
|
|
6611
|
+
* const result = await agent.sendDirect('did:voidly:peer', 'Hello P2P!');
|
|
6612
|
+
* console.log(result.direct); // true if delivered directly, false if via relay
|
|
6613
|
+
* ```
|
|
6614
|
+
*/
|
|
6615
|
+
async sendDirect(recipientDid, message, options = {}) {
|
|
6616
|
+
try {
|
|
6617
|
+
const profile = await this.getIdentity(recipientDid);
|
|
6618
|
+
if (profile) {
|
|
6619
|
+
const webhookRes = await this._timedFetch(
|
|
6620
|
+
`${this.baseUrl}/v1/agent/identity/${recipientDid}`,
|
|
6621
|
+
{ headers: { "X-Agent-Key": this.apiKey } }
|
|
6622
|
+
);
|
|
6623
|
+
if (webhookRes.ok) {
|
|
6624
|
+
const data = await webhookRes.json();
|
|
6625
|
+
const webhookUrl = data.webhook_url;
|
|
6626
|
+
if (webhookUrl) {
|
|
6627
|
+
const nonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.box.nonceLength);
|
|
6628
|
+
const recipientPubKey = (0, import_tweetnacl_util.decodeBase64)(profile.encryption_public_key);
|
|
6629
|
+
const plaintext = (0, import_tweetnacl_util.decodeUTF8)(message);
|
|
6630
|
+
const ciphertext = import_tweetnacl.default.box(plaintext, nonce, recipientPubKey, this.encryptionKeyPair.secretKey);
|
|
6631
|
+
const envelope = JSON.stringify({
|
|
6632
|
+
from: this.did,
|
|
6633
|
+
to: recipientDid,
|
|
6634
|
+
ciphertext: (0, import_tweetnacl_util.encodeBase64)(ciphertext),
|
|
6635
|
+
nonce: (0, import_tweetnacl_util.encodeBase64)(nonce),
|
|
6636
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6637
|
+
message_type: options.messageType || "text",
|
|
6638
|
+
thread_id: options.threadId
|
|
6639
|
+
});
|
|
6640
|
+
const signature = import_tweetnacl.default.sign.detached((0, import_tweetnacl_util.decodeUTF8)(envelope), this.signingKeyPair.secretKey);
|
|
6641
|
+
const directRes = await this._timedFetch(webhookUrl, {
|
|
6642
|
+
method: "POST",
|
|
6643
|
+
headers: {
|
|
6644
|
+
"Content-Type": "application/json",
|
|
6645
|
+
"X-Voidly-Signature": `sha256=${(0, import_tweetnacl_util.encodeBase64)(signature)}`,
|
|
6646
|
+
"X-Voidly-Sender": this.did
|
|
6647
|
+
},
|
|
6648
|
+
body: envelope
|
|
6649
|
+
});
|
|
6650
|
+
if (directRes.ok) {
|
|
6651
|
+
const now = /* @__PURE__ */ new Date();
|
|
6652
|
+
return {
|
|
6653
|
+
id: `direct-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
6654
|
+
from: this.did,
|
|
6655
|
+
to: recipientDid,
|
|
6656
|
+
timestamp: now.toISOString(),
|
|
6657
|
+
expiresAt: new Date(now.getTime() + 864e5).toISOString(),
|
|
6658
|
+
encrypted: true,
|
|
6659
|
+
clientSide: true,
|
|
6660
|
+
direct: true
|
|
6661
|
+
};
|
|
6662
|
+
}
|
|
6663
|
+
}
|
|
6664
|
+
}
|
|
6665
|
+
}
|
|
6666
|
+
} catch {
|
|
6667
|
+
}
|
|
6668
|
+
const result = await this.send(recipientDid, message, options);
|
|
6669
|
+
return { ...result, direct: false };
|
|
6670
|
+
}
|
|
6671
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6672
|
+
// COVER TRAFFIC — Noise Protocol for Traffic Analysis Resistance
|
|
6673
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6674
|
+
/**
|
|
6675
|
+
* Enable cover traffic — sends encrypted noise at random intervals.
|
|
6676
|
+
* Makes real messages indistinguishable from cover traffic for any observer
|
|
6677
|
+
* monitoring message timing and frequency.
|
|
6678
|
+
*
|
|
6679
|
+
* Cover messages are encrypted and padded identically to real messages.
|
|
6680
|
+
* The relay cannot distinguish them from real traffic.
|
|
6681
|
+
*
|
|
6682
|
+
* @example
|
|
6683
|
+
* ```ts
|
|
6684
|
+
* // Send noise every ~30s (randomized ±50%)
|
|
6685
|
+
* agent.enableCoverTraffic({ intervalMs: 30000 });
|
|
6686
|
+
*
|
|
6687
|
+
* // Stop cover traffic
|
|
6688
|
+
* agent.disableCoverTraffic();
|
|
6689
|
+
* ```
|
|
6690
|
+
*/
|
|
6691
|
+
enableCoverTraffic(options = {}) {
|
|
6692
|
+
this.disableCoverTraffic();
|
|
6693
|
+
const baseInterval = options.intervalMs || 3e4;
|
|
6694
|
+
const sendNoise = async () => {
|
|
6695
|
+
try {
|
|
6696
|
+
const noise = import_tweetnacl.default.randomBytes(128 + Math.floor(Math.random() * 384));
|
|
6697
|
+
await this.send(this.did, (0, import_tweetnacl_util.encodeBase64)(noise), {
|
|
6698
|
+
messageType: "ping",
|
|
6699
|
+
// use 'ping' type — indistinguishable in encrypted payload
|
|
6700
|
+
ttl: 60
|
|
6701
|
+
// short TTL — noise auto-expires
|
|
6702
|
+
});
|
|
6703
|
+
} catch {
|
|
6704
|
+
}
|
|
6705
|
+
};
|
|
6706
|
+
const scheduleNext = () => {
|
|
6707
|
+
const jitter = baseInterval * (0.5 + Math.random());
|
|
6708
|
+
this._coverTrafficTimer = setTimeout(async () => {
|
|
6709
|
+
await sendNoise();
|
|
6710
|
+
if (this._coverTrafficTimer !== null) scheduleNext();
|
|
6711
|
+
}, jitter);
|
|
6712
|
+
};
|
|
6713
|
+
scheduleNext();
|
|
6714
|
+
}
|
|
6715
|
+
/**
|
|
6716
|
+
* Disable cover traffic.
|
|
6717
|
+
*/
|
|
6718
|
+
disableCoverTraffic() {
|
|
6719
|
+
if (this._coverTrafficTimer !== null) {
|
|
6720
|
+
clearTimeout(this._coverTrafficTimer);
|
|
6721
|
+
this._coverTrafficTimer = null;
|
|
6722
|
+
}
|
|
6723
|
+
}
|
|
6724
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6725
|
+
// RESILIENT OPERATIONS — Fallback for All Operations
|
|
6726
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6727
|
+
/**
|
|
6728
|
+
* Fetch from primary relay with fallback to alternate relays.
|
|
6729
|
+
* Unlike _timedFetch which only hits one URL, this tries all known relays.
|
|
6730
|
+
* @internal
|
|
6731
|
+
*/
|
|
6732
|
+
async _resilientFetch(path, init) {
|
|
6733
|
+
const relays = [this.baseUrl, ...this.fallbackRelays];
|
|
6734
|
+
let lastError = null;
|
|
6735
|
+
for (const relay of relays) {
|
|
6736
|
+
try {
|
|
6737
|
+
const res = await this._timedFetch(`${relay}${path}`, init);
|
|
6738
|
+
if (res.ok || res.status >= 400 && res.status < 500) return res;
|
|
6739
|
+
} catch (err) {
|
|
6740
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
6741
|
+
}
|
|
6742
|
+
}
|
|
6743
|
+
throw lastError || new Error("All relays failed");
|
|
6183
6744
|
}
|
|
6184
6745
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
6185
6746
|
// CONVERSATIONS — Thread Management
|
|
@@ -6318,13 +6879,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6318
6879
|
return {
|
|
6319
6880
|
relayCanSee: [
|
|
6320
6881
|
"Your DID (public identifier)",
|
|
6321
|
-
|
|
6882
|
+
"Recipient DIDs (relay needs them for routing)",
|
|
6322
6883
|
...this.jitterMs > 0 ? ["Approximate timing (jittered timestamps)"] : ["When you message (timestamps)"],
|
|
6323
|
-
"Message types
|
|
6324
|
-
"Thread structure (which messages are replies)",
|
|
6884
|
+
...this.sealedSender ? [] : ["Message types, thread IDs, content types (sent in cleartext without sealed sender)"],
|
|
6325
6885
|
"Channel membership (but NOT channel message content with client-side encryption)",
|
|
6326
6886
|
"Capability registrations",
|
|
6327
|
-
"Online/offline status",
|
|
6328
6887
|
"Approximate message size (even with padding, bounded to power-of-2)"
|
|
6329
6888
|
],
|
|
6330
6889
|
relayCannotSee: [
|
|
@@ -6334,7 +6893,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6334
6893
|
"Past message keys (forward secrecy \u2014 hash ratchet + DH ratchet, old keys deleted)",
|
|
6335
6894
|
"Future message keys (post-compromise recovery via DH ratchet \u2014 compromise heals after one round-trip)",
|
|
6336
6895
|
"Channel message content (client-side nacl.secretbox encryption \u2014 relay stores only ciphertext)",
|
|
6337
|
-
...this.sealedSender ? [
|
|
6896
|
+
...this.sealedSender ? [
|
|
6897
|
+
'Sender identity (sealed inside ciphertext \u2014 relay stores "sealed" not your DID)',
|
|
6898
|
+
"Message types, thread IDs, reply chains (packed inside ciphertext in v3)",
|
|
6899
|
+
"Message count (not incremented for sealed senders)"
|
|
6900
|
+
] : [],
|
|
6338
6901
|
...this.deniable ? ["Who authored a message (HMAC is symmetric \u2014 either party could have produced it)"] : []
|
|
6339
6902
|
],
|
|
6340
6903
|
protections: [
|
|
@@ -6352,22 +6915,34 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6352
6915
|
"Request timeouts (AbortController on all HTTP, configurable)",
|
|
6353
6916
|
"Request validation (fromCredentials validates key sizes and format)",
|
|
6354
6917
|
...this.paddingEnabled ? ["Message padding to power-of-2 boundary (traffic analysis resistance)"] : [],
|
|
6355
|
-
...this.sealedSender ? [
|
|
6918
|
+
...this.sealedSender ? [
|
|
6919
|
+
"Sealed sender (relay cannot see who sent a message)",
|
|
6920
|
+
"Metadata privacy (v3 \u2014 thread_id, message_type, reply_to packed inside ciphertext, stripped from relay storage)"
|
|
6921
|
+
] : [],
|
|
6356
6922
|
...this.requireSignatures ? ["Strict signature enforcement (reject unsigned/invalid messages)"] : [],
|
|
6357
|
-
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays)`] : [],
|
|
6923
|
+
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays \u2014 receive, discover, identity all use fallbacks)`] : [],
|
|
6358
6924
|
...this.jitterMs > 0 ? [`Timing jitter (random ${this.jitterMs}ms delay \u2014 metadata timing protection)`] : [],
|
|
6925
|
+
...this._transportPrefs.includes("sse") ? ["SSE streaming transport (real-time push delivery from relay)"] : [],
|
|
6359
6926
|
...this.longPoll ? ["Long-poll transport (25s server-held connection \u2014 near-real-time delivery)"] : [],
|
|
6927
|
+
...this._persistMode !== "memory" ? [`Ratchet state auto-persistence (${this._persistMode} backend \u2014 survives process restart)`] : [],
|
|
6928
|
+
...this._coverTrafficTimer !== null ? ["Cover traffic (encrypted noise at random intervals \u2014 traffic analysis resistance)"] : [],
|
|
6929
|
+
"Agent RPC (invoke/onInvoke \u2014 synchronous function calls between agents)",
|
|
6930
|
+
"P2P direct send (bypass relay via webhook \u2014 true peer-to-peer when possible)",
|
|
6931
|
+
"Resilient operations (receive, discover, identity \u2014 all try fallback relays)",
|
|
6360
6932
|
"Auto-retry with exponential backoff",
|
|
6361
6933
|
"Offline message queue",
|
|
6362
6934
|
"did:key interoperability (W3C standard DID format)"
|
|
6363
6935
|
],
|
|
6364
6936
|
gaps: [
|
|
6365
6937
|
...!this.postQuantum || !this.mlkemPublicKey ? ["No post-quantum protection \u2014 enable postQuantum option and re-register"] : [],
|
|
6366
|
-
"
|
|
6367
|
-
"
|
|
6938
|
+
...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"],
|
|
6939
|
+
"Relay sees channel membership, task delegation, trust scores (social graph)",
|
|
6940
|
+
...this.fallbackRelays.length === 0 ? ["Single relay with no fallbacks \u2014 configure fallbackRelays for resilience"] : [],
|
|
6941
|
+
...this._persistMode === "memory" ? ["Ratchet state is in-memory (lost on process restart \u2014 use persist option or exportCredentials)"] : [],
|
|
6368
6942
|
...!this.deniable ? ["Ed25519 signatures are non-repudiable \u2014 enable deniable option for HMAC auth"] : [],
|
|
6369
6943
|
...!this.doubleRatchet ? ["Hash ratchet only \u2014 enable doubleRatchet option for post-compromise recovery"] : [],
|
|
6370
|
-
...this.jitterMs === 0 ? ["No timing jitter \u2014 enable jitterMs option for metadata protection"] : []
|
|
6944
|
+
...this.jitterMs === 0 ? ["No timing jitter \u2014 enable jitterMs option for metadata protection"] : [],
|
|
6945
|
+
...this._coverTrafficTimer === null ? ["No cover traffic \u2014 call enableCoverTraffic() to resist traffic analysis"] : []
|
|
6371
6946
|
]
|
|
6372
6947
|
};
|
|
6373
6948
|
}
|