@voidly/agent-sdk 3.0.0 → 3.1.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 +98 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +368 -31
- package/dist/index.mjs +368 -31
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -181,6 +181,10 @@ declare class VoidlyAgent {
|
|
|
181
181
|
private _identityCache;
|
|
182
182
|
private _seenMessageIds;
|
|
183
183
|
private _decryptFailCount;
|
|
184
|
+
private _rpcHandlers;
|
|
185
|
+
private _rpcPending;
|
|
186
|
+
private _coverTrafficTimer;
|
|
187
|
+
private _rpcListener;
|
|
184
188
|
private constructor();
|
|
185
189
|
/**
|
|
186
190
|
* Register a new agent on the Voidly relay.
|
|
@@ -996,6 +1000,100 @@ declare class VoidlyAgent {
|
|
|
996
1000
|
* Stop all active listeners. Useful for clean shutdown.
|
|
997
1001
|
*/
|
|
998
1002
|
stopAll(): void;
|
|
1003
|
+
/**
|
|
1004
|
+
* Invoke a function on a remote agent. Synchronous RPC over encrypted messaging.
|
|
1005
|
+
* The remote agent must have registered a handler via `onInvoke()`.
|
|
1006
|
+
*
|
|
1007
|
+
* @example
|
|
1008
|
+
* ```ts
|
|
1009
|
+
* // Call a translator agent
|
|
1010
|
+
* const result = await agent.invoke('did:voidly:translator', 'translate', {
|
|
1011
|
+
* text: 'Hello, world!',
|
|
1012
|
+
* to: 'ja',
|
|
1013
|
+
* });
|
|
1014
|
+
* console.log(result.translation); // こんにちは
|
|
1015
|
+
*
|
|
1016
|
+
* // With timeout
|
|
1017
|
+
* const data = await agent.invoke(peerDid, 'analyze', { url: '...' }, 15000);
|
|
1018
|
+
* ```
|
|
1019
|
+
*/
|
|
1020
|
+
invoke(targetDid: string, method: string, params?: any, timeoutMs?: number): Promise<any>;
|
|
1021
|
+
/**
|
|
1022
|
+
* Register a handler for incoming RPC invocations.
|
|
1023
|
+
* When another agent calls `invoke(yourDid, method, params)`, your handler runs.
|
|
1024
|
+
*
|
|
1025
|
+
* @example
|
|
1026
|
+
* ```ts
|
|
1027
|
+
* // Register a translation capability
|
|
1028
|
+
* agent.onInvoke('translate', async (params, callerDid) => {
|
|
1029
|
+
* const result = await myTranslateFunction(params.text, params.to);
|
|
1030
|
+
* return { translation: result };
|
|
1031
|
+
* });
|
|
1032
|
+
*
|
|
1033
|
+
* // Register a search capability
|
|
1034
|
+
* agent.onInvoke('search', async (params) => {
|
|
1035
|
+
* return { results: await searchDatabase(params.query) };
|
|
1036
|
+
* });
|
|
1037
|
+
* ```
|
|
1038
|
+
*/
|
|
1039
|
+
onInvoke(method: string, handler: (params: any, callerDid: string) => Promise<any>): void;
|
|
1040
|
+
/**
|
|
1041
|
+
* Remove an RPC handler.
|
|
1042
|
+
*/
|
|
1043
|
+
offInvoke(method: string): void;
|
|
1044
|
+
/** @internal Start listening for RPC requests and responses */
|
|
1045
|
+
private _ensureRpcListener;
|
|
1046
|
+
/**
|
|
1047
|
+
* Send a message directly to a peer's webhook endpoint, bypassing the relay entirely.
|
|
1048
|
+
* The relay never sees the message — true peer-to-peer encrypted delivery.
|
|
1049
|
+
*
|
|
1050
|
+
* Falls back to relay-based send if direct delivery fails.
|
|
1051
|
+
*
|
|
1052
|
+
* @example
|
|
1053
|
+
* ```ts
|
|
1054
|
+
* // Try direct first, fall back to relay
|
|
1055
|
+
* const result = await agent.sendDirect('did:voidly:peer', 'Hello P2P!');
|
|
1056
|
+
* console.log(result.direct); // true if delivered directly, false if via relay
|
|
1057
|
+
* ```
|
|
1058
|
+
*/
|
|
1059
|
+
sendDirect(recipientDid: string, message: string, options?: {
|
|
1060
|
+
contentType?: string;
|
|
1061
|
+
messageType?: string;
|
|
1062
|
+
threadId?: string;
|
|
1063
|
+
ttl?: number;
|
|
1064
|
+
}): Promise<SendResult & {
|
|
1065
|
+
direct: boolean;
|
|
1066
|
+
}>;
|
|
1067
|
+
/**
|
|
1068
|
+
* Enable cover traffic — sends encrypted noise at random intervals.
|
|
1069
|
+
* Makes real messages indistinguishable from cover traffic for any observer
|
|
1070
|
+
* monitoring message timing and frequency.
|
|
1071
|
+
*
|
|
1072
|
+
* Cover messages are encrypted and padded identically to real messages.
|
|
1073
|
+
* The relay cannot distinguish them from real traffic.
|
|
1074
|
+
*
|
|
1075
|
+
* @example
|
|
1076
|
+
* ```ts
|
|
1077
|
+
* // Send noise every ~30s (randomized ±50%)
|
|
1078
|
+
* agent.enableCoverTraffic({ intervalMs: 30000 });
|
|
1079
|
+
*
|
|
1080
|
+
* // Stop cover traffic
|
|
1081
|
+
* agent.disableCoverTraffic();
|
|
1082
|
+
* ```
|
|
1083
|
+
*/
|
|
1084
|
+
enableCoverTraffic(options?: {
|
|
1085
|
+
intervalMs?: number;
|
|
1086
|
+
}): void;
|
|
1087
|
+
/**
|
|
1088
|
+
* Disable cover traffic.
|
|
1089
|
+
*/
|
|
1090
|
+
disableCoverTraffic(): void;
|
|
1091
|
+
/**
|
|
1092
|
+
* Fetch from primary relay with fallback to alternate relays.
|
|
1093
|
+
* Unlike _timedFetch which only hits one URL, this tries all known relays.
|
|
1094
|
+
* @internal
|
|
1095
|
+
*/
|
|
1096
|
+
private _resilientFetch;
|
|
999
1097
|
/**
|
|
1000
1098
|
* Start or resume a conversation with another agent.
|
|
1001
1099
|
* Automatically manages thread IDs, message history, and reply chains.
|
package/dist/index.d.ts
CHANGED
|
@@ -181,6 +181,10 @@ declare class VoidlyAgent {
|
|
|
181
181
|
private _identityCache;
|
|
182
182
|
private _seenMessageIds;
|
|
183
183
|
private _decryptFailCount;
|
|
184
|
+
private _rpcHandlers;
|
|
185
|
+
private _rpcPending;
|
|
186
|
+
private _coverTrafficTimer;
|
|
187
|
+
private _rpcListener;
|
|
184
188
|
private constructor();
|
|
185
189
|
/**
|
|
186
190
|
* Register a new agent on the Voidly relay.
|
|
@@ -996,6 +1000,100 @@ declare class VoidlyAgent {
|
|
|
996
1000
|
* Stop all active listeners. Useful for clean shutdown.
|
|
997
1001
|
*/
|
|
998
1002
|
stopAll(): void;
|
|
1003
|
+
/**
|
|
1004
|
+
* Invoke a function on a remote agent. Synchronous RPC over encrypted messaging.
|
|
1005
|
+
* The remote agent must have registered a handler via `onInvoke()`.
|
|
1006
|
+
*
|
|
1007
|
+
* @example
|
|
1008
|
+
* ```ts
|
|
1009
|
+
* // Call a translator agent
|
|
1010
|
+
* const result = await agent.invoke('did:voidly:translator', 'translate', {
|
|
1011
|
+
* text: 'Hello, world!',
|
|
1012
|
+
* to: 'ja',
|
|
1013
|
+
* });
|
|
1014
|
+
* console.log(result.translation); // こんにちは
|
|
1015
|
+
*
|
|
1016
|
+
* // With timeout
|
|
1017
|
+
* const data = await agent.invoke(peerDid, 'analyze', { url: '...' }, 15000);
|
|
1018
|
+
* ```
|
|
1019
|
+
*/
|
|
1020
|
+
invoke(targetDid: string, method: string, params?: any, timeoutMs?: number): Promise<any>;
|
|
1021
|
+
/**
|
|
1022
|
+
* Register a handler for incoming RPC invocations.
|
|
1023
|
+
* When another agent calls `invoke(yourDid, method, params)`, your handler runs.
|
|
1024
|
+
*
|
|
1025
|
+
* @example
|
|
1026
|
+
* ```ts
|
|
1027
|
+
* // Register a translation capability
|
|
1028
|
+
* agent.onInvoke('translate', async (params, callerDid) => {
|
|
1029
|
+
* const result = await myTranslateFunction(params.text, params.to);
|
|
1030
|
+
* return { translation: result };
|
|
1031
|
+
* });
|
|
1032
|
+
*
|
|
1033
|
+
* // Register a search capability
|
|
1034
|
+
* agent.onInvoke('search', async (params) => {
|
|
1035
|
+
* return { results: await searchDatabase(params.query) };
|
|
1036
|
+
* });
|
|
1037
|
+
* ```
|
|
1038
|
+
*/
|
|
1039
|
+
onInvoke(method: string, handler: (params: any, callerDid: string) => Promise<any>): void;
|
|
1040
|
+
/**
|
|
1041
|
+
* Remove an RPC handler.
|
|
1042
|
+
*/
|
|
1043
|
+
offInvoke(method: string): void;
|
|
1044
|
+
/** @internal Start listening for RPC requests and responses */
|
|
1045
|
+
private _ensureRpcListener;
|
|
1046
|
+
/**
|
|
1047
|
+
* Send a message directly to a peer's webhook endpoint, bypassing the relay entirely.
|
|
1048
|
+
* The relay never sees the message — true peer-to-peer encrypted delivery.
|
|
1049
|
+
*
|
|
1050
|
+
* Falls back to relay-based send if direct delivery fails.
|
|
1051
|
+
*
|
|
1052
|
+
* @example
|
|
1053
|
+
* ```ts
|
|
1054
|
+
* // Try direct first, fall back to relay
|
|
1055
|
+
* const result = await agent.sendDirect('did:voidly:peer', 'Hello P2P!');
|
|
1056
|
+
* console.log(result.direct); // true if delivered directly, false if via relay
|
|
1057
|
+
* ```
|
|
1058
|
+
*/
|
|
1059
|
+
sendDirect(recipientDid: string, message: string, options?: {
|
|
1060
|
+
contentType?: string;
|
|
1061
|
+
messageType?: string;
|
|
1062
|
+
threadId?: string;
|
|
1063
|
+
ttl?: number;
|
|
1064
|
+
}): Promise<SendResult & {
|
|
1065
|
+
direct: boolean;
|
|
1066
|
+
}>;
|
|
1067
|
+
/**
|
|
1068
|
+
* Enable cover traffic — sends encrypted noise at random intervals.
|
|
1069
|
+
* Makes real messages indistinguishable from cover traffic for any observer
|
|
1070
|
+
* monitoring message timing and frequency.
|
|
1071
|
+
*
|
|
1072
|
+
* Cover messages are encrypted and padded identically to real messages.
|
|
1073
|
+
* The relay cannot distinguish them from real traffic.
|
|
1074
|
+
*
|
|
1075
|
+
* @example
|
|
1076
|
+
* ```ts
|
|
1077
|
+
* // Send noise every ~30s (randomized ±50%)
|
|
1078
|
+
* agent.enableCoverTraffic({ intervalMs: 30000 });
|
|
1079
|
+
*
|
|
1080
|
+
* // Stop cover traffic
|
|
1081
|
+
* agent.disableCoverTraffic();
|
|
1082
|
+
* ```
|
|
1083
|
+
*/
|
|
1084
|
+
enableCoverTraffic(options?: {
|
|
1085
|
+
intervalMs?: number;
|
|
1086
|
+
}): void;
|
|
1087
|
+
/**
|
|
1088
|
+
* Disable cover traffic.
|
|
1089
|
+
*/
|
|
1090
|
+
disableCoverTraffic(): void;
|
|
1091
|
+
/**
|
|
1092
|
+
* Fetch from primary relay with fallback to alternate relays.
|
|
1093
|
+
* Unlike _timedFetch which only hits one URL, this tries all known relays.
|
|
1094
|
+
* @internal
|
|
1095
|
+
*/
|
|
1096
|
+
private _resilientFetch;
|
|
999
1097
|
/**
|
|
1000
1098
|
* Start or resume a conversation with another agent.
|
|
1001
1099
|
* Automatically manages thread IDs, message history, and reply chains.
|
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,14 @@ 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;
|
|
4076
4094
|
this.did = identity.did;
|
|
4077
4095
|
this.apiKey = identity.apiKey;
|
|
4078
4096
|
this.signingKeyPair = identity.signingKeyPair;
|
|
@@ -4343,7 +4361,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4343
4361
|
const recipientPubKey = (0, import_tweetnacl_util.decodeBase64)(profile.encryption_public_key);
|
|
4344
4362
|
let plaintext = message;
|
|
4345
4363
|
if (useSealed) {
|
|
4346
|
-
plaintext = sealEnvelope(this.did, message
|
|
4364
|
+
plaintext = sealEnvelope(this.did, message, {
|
|
4365
|
+
contentType: options.contentType,
|
|
4366
|
+
messageType: options.messageType,
|
|
4367
|
+
threadId: options.threadId,
|
|
4368
|
+
replyTo: options.replyTo
|
|
4369
|
+
});
|
|
4347
4370
|
}
|
|
4348
4371
|
let contentBytes;
|
|
4349
4372
|
if (usePadding) {
|
|
@@ -4458,12 +4481,14 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4458
4481
|
nonce: (0, import_tweetnacl_util.encodeBase64)(nonce),
|
|
4459
4482
|
signature: (0, import_tweetnacl_util.encodeBase64)(signature),
|
|
4460
4483
|
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
4484
|
ttl: options.ttl
|
|
4466
4485
|
};
|
|
4486
|
+
if (!useSealed) {
|
|
4487
|
+
payload.content_type = options.contentType || "text/plain";
|
|
4488
|
+
payload.message_type = options.messageType || "text";
|
|
4489
|
+
payload.thread_id = options.threadId;
|
|
4490
|
+
payload.reply_to = options.replyTo;
|
|
4491
|
+
}
|
|
4467
4492
|
const relays = [this.baseUrl, ...this.fallbackRelays];
|
|
4468
4493
|
let lastError = null;
|
|
4469
4494
|
for (const relay of relays) {
|
|
@@ -4514,7 +4539,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4514
4539
|
if (options.contentType) params.set("content_type", options.contentType);
|
|
4515
4540
|
if (options.messageType) params.set("message_type", options.messageType);
|
|
4516
4541
|
if (options.unreadOnly) params.set("unread", "true");
|
|
4517
|
-
const res = await this.
|
|
4542
|
+
const res = await this._resilientFetch(`/v1/agent/receive/raw?${params}`, {
|
|
4518
4543
|
headers: { "X-Agent-Key": this.apiKey }
|
|
4519
4544
|
});
|
|
4520
4545
|
if (!res.ok) {
|
|
@@ -4526,7 +4551,20 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4526
4551
|
for (const msg of data.messages) {
|
|
4527
4552
|
try {
|
|
4528
4553
|
if (this._seenMessageIds.has(msg.id)) continue;
|
|
4529
|
-
|
|
4554
|
+
let senderEncPub;
|
|
4555
|
+
let senderSignPubBytes = null;
|
|
4556
|
+
if (msg.sender_encryption_key) {
|
|
4557
|
+
senderEncPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_encryption_key);
|
|
4558
|
+
if (msg.sender_signing_key) senderSignPubBytes = (0, import_tweetnacl_util.decodeBase64)(msg.sender_signing_key);
|
|
4559
|
+
} else if (msg.envelope) {
|
|
4560
|
+
const env = JSON.parse(msg.envelope);
|
|
4561
|
+
const senderProfile = await this.getIdentity(env.from);
|
|
4562
|
+
if (!senderProfile) continue;
|
|
4563
|
+
senderEncPub = (0, import_tweetnacl_util.decodeBase64)(senderProfile.encryption_public_key);
|
|
4564
|
+
if (senderProfile.signing_public_key) senderSignPubBytes = (0, import_tweetnacl_util.decodeBase64)(senderProfile.signing_public_key);
|
|
4565
|
+
} else {
|
|
4566
|
+
continue;
|
|
4567
|
+
}
|
|
4530
4568
|
const ciphertext = (0, import_tweetnacl_util.decodeBase64)(msg.ciphertext);
|
|
4531
4569
|
const nonce = (0, import_tweetnacl_util.decodeBase64)(msg.nonce);
|
|
4532
4570
|
let rawPlaintext = null;
|
|
@@ -4694,11 +4732,19 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4694
4732
|
}
|
|
4695
4733
|
let content = (0, import_tweetnacl_util.encodeUTF8)(plaintextBytes);
|
|
4696
4734
|
let senderDid = msg.from;
|
|
4735
|
+
let innerContentType;
|
|
4736
|
+
let innerMessageType;
|
|
4737
|
+
let innerThreadId;
|
|
4738
|
+
let innerReplyTo;
|
|
4697
4739
|
if (wasSealed || !proto) {
|
|
4698
4740
|
const unsealed = unsealEnvelope(content);
|
|
4699
4741
|
if (unsealed) {
|
|
4700
4742
|
content = unsealed.msg;
|
|
4701
4743
|
senderDid = unsealed.from;
|
|
4744
|
+
innerContentType = unsealed.contentType;
|
|
4745
|
+
innerMessageType = unsealed.messageType;
|
|
4746
|
+
innerThreadId = unsealed.threadId;
|
|
4747
|
+
innerReplyTo = unsealed.replyTo;
|
|
4702
4748
|
}
|
|
4703
4749
|
}
|
|
4704
4750
|
let signatureValid = false;
|
|
@@ -4719,12 +4765,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4719
4765
|
for (let i = 0; i < expectedHmac.length; i++) diff |= expectedHmac[i] ^ signatureBytes[i];
|
|
4720
4766
|
signatureValid = diff === 0;
|
|
4721
4767
|
}
|
|
4722
|
-
} else {
|
|
4723
|
-
const senderSignPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_signing_key);
|
|
4768
|
+
} else if (senderSignPubBytes) {
|
|
4724
4769
|
signatureValid = import_tweetnacl.default.sign.detached.verify(
|
|
4725
4770
|
(0, import_tweetnacl_util.decodeUTF8)(envelopeStr),
|
|
4726
4771
|
signatureBytes,
|
|
4727
|
-
|
|
4772
|
+
senderSignPubBytes
|
|
4728
4773
|
);
|
|
4729
4774
|
}
|
|
4730
4775
|
} catch {
|
|
@@ -4744,10 +4789,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4744
4789
|
from: senderDid,
|
|
4745
4790
|
to: msg.to,
|
|
4746
4791
|
content,
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4792
|
+
// v3: prefer metadata from inside ciphertext (relay can't see it)
|
|
4793
|
+
contentType: innerContentType || msg.content_type || "text/plain",
|
|
4794
|
+
messageType: innerMessageType || msg.message_type || "text",
|
|
4795
|
+
threadId: innerThreadId || msg.thread_id || null,
|
|
4796
|
+
replyTo: innerReplyTo || msg.reply_to || null,
|
|
4751
4797
|
signatureValid,
|
|
4752
4798
|
timestamp: msg.timestamp,
|
|
4753
4799
|
expiresAt: msg.expires_at
|
|
@@ -4808,7 +4854,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4808
4854
|
if (cached && Date.now() - cached.cachedAt < 3e5) {
|
|
4809
4855
|
return cached.profile;
|
|
4810
4856
|
}
|
|
4811
|
-
const res = await this.
|
|
4857
|
+
const res = await this._resilientFetch(`/v1/agent/identity/${did}`);
|
|
4812
4858
|
if (!res.ok) return null;
|
|
4813
4859
|
const profile = await res.json();
|
|
4814
4860
|
this._identityCache.set(did, { profile, cachedAt: Date.now() });
|
|
@@ -4826,7 +4872,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4826
4872
|
if (options.query) params.set("query", options.query);
|
|
4827
4873
|
if (options.capability) params.set("capability", options.capability);
|
|
4828
4874
|
if (options.limit) params.set("limit", String(options.limit));
|
|
4829
|
-
const res = await this.
|
|
4875
|
+
const res = await this._resilientFetch(`/v1/agent/discover?${params}`);
|
|
4830
4876
|
if (!res.ok) return [];
|
|
4831
4877
|
const data = await res.json();
|
|
4832
4878
|
return data.agents;
|
|
@@ -6180,6 +6226,285 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6180
6226
|
listener.stop();
|
|
6181
6227
|
}
|
|
6182
6228
|
this._listeners.clear();
|
|
6229
|
+
if (this._rpcListener) {
|
|
6230
|
+
this._rpcListener.stop();
|
|
6231
|
+
this._rpcListener = null;
|
|
6232
|
+
}
|
|
6233
|
+
for (const [id, pending] of this._rpcPending) {
|
|
6234
|
+
clearTimeout(pending.timer);
|
|
6235
|
+
pending.reject(new Error("Agent stopped"));
|
|
6236
|
+
}
|
|
6237
|
+
this._rpcPending.clear();
|
|
6238
|
+
this.disableCoverTraffic();
|
|
6239
|
+
}
|
|
6240
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6241
|
+
// AGENT RPC — Synchronous Function Invocation Between Agents
|
|
6242
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6243
|
+
/**
|
|
6244
|
+
* Invoke a function on a remote agent. Synchronous RPC over encrypted messaging.
|
|
6245
|
+
* The remote agent must have registered a handler via `onInvoke()`.
|
|
6246
|
+
*
|
|
6247
|
+
* @example
|
|
6248
|
+
* ```ts
|
|
6249
|
+
* // Call a translator agent
|
|
6250
|
+
* const result = await agent.invoke('did:voidly:translator', 'translate', {
|
|
6251
|
+
* text: 'Hello, world!',
|
|
6252
|
+
* to: 'ja',
|
|
6253
|
+
* });
|
|
6254
|
+
* console.log(result.translation); // こんにちは
|
|
6255
|
+
*
|
|
6256
|
+
* // With timeout
|
|
6257
|
+
* const data = await agent.invoke(peerDid, 'analyze', { url: '...' }, 15000);
|
|
6258
|
+
* ```
|
|
6259
|
+
*/
|
|
6260
|
+
async invoke(targetDid, method, params = {}, timeoutMs = 3e4) {
|
|
6261
|
+
const rpcId = `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
6262
|
+
return new Promise(async (resolve, reject) => {
|
|
6263
|
+
const timer = setTimeout(() => {
|
|
6264
|
+
this._rpcPending.delete(rpcId);
|
|
6265
|
+
reject(new Error(`RPC timeout: ${method}@${targetDid} after ${timeoutMs}ms`));
|
|
6266
|
+
}, timeoutMs);
|
|
6267
|
+
this._rpcPending.set(rpcId, { resolve, reject, timer });
|
|
6268
|
+
try {
|
|
6269
|
+
await this.send(targetDid, JSON.stringify({
|
|
6270
|
+
jsonrpc: "2.0",
|
|
6271
|
+
method,
|
|
6272
|
+
params,
|
|
6273
|
+
id: rpcId
|
|
6274
|
+
}), { messageType: "rpc-request", threadId: rpcId });
|
|
6275
|
+
} catch (err) {
|
|
6276
|
+
clearTimeout(timer);
|
|
6277
|
+
this._rpcPending.delete(rpcId);
|
|
6278
|
+
reject(err);
|
|
6279
|
+
}
|
|
6280
|
+
});
|
|
6281
|
+
}
|
|
6282
|
+
/**
|
|
6283
|
+
* Register a handler for incoming RPC invocations.
|
|
6284
|
+
* When another agent calls `invoke(yourDid, method, params)`, your handler runs.
|
|
6285
|
+
*
|
|
6286
|
+
* @example
|
|
6287
|
+
* ```ts
|
|
6288
|
+
* // Register a translation capability
|
|
6289
|
+
* agent.onInvoke('translate', async (params, callerDid) => {
|
|
6290
|
+
* const result = await myTranslateFunction(params.text, params.to);
|
|
6291
|
+
* return { translation: result };
|
|
6292
|
+
* });
|
|
6293
|
+
*
|
|
6294
|
+
* // Register a search capability
|
|
6295
|
+
* agent.onInvoke('search', async (params) => {
|
|
6296
|
+
* return { results: await searchDatabase(params.query) };
|
|
6297
|
+
* });
|
|
6298
|
+
* ```
|
|
6299
|
+
*/
|
|
6300
|
+
onInvoke(method, handler) {
|
|
6301
|
+
this._rpcHandlers.set(method, handler);
|
|
6302
|
+
this._ensureRpcListener();
|
|
6303
|
+
}
|
|
6304
|
+
/**
|
|
6305
|
+
* Remove an RPC handler.
|
|
6306
|
+
*/
|
|
6307
|
+
offInvoke(method) {
|
|
6308
|
+
this._rpcHandlers.delete(method);
|
|
6309
|
+
if (this._rpcHandlers.size === 0 && this._rpcListener) {
|
|
6310
|
+
this._rpcListener.stop();
|
|
6311
|
+
this._rpcListener = null;
|
|
6312
|
+
}
|
|
6313
|
+
}
|
|
6314
|
+
/** @internal Start listening for RPC requests and responses */
|
|
6315
|
+
_ensureRpcListener() {
|
|
6316
|
+
if (this._rpcListener) return;
|
|
6317
|
+
this._rpcListener = this.listen(async (msg) => {
|
|
6318
|
+
try {
|
|
6319
|
+
const payload = JSON.parse(msg.content);
|
|
6320
|
+
if (payload.jsonrpc !== "2.0") return;
|
|
6321
|
+
if (payload.id && (payload.result !== void 0 || payload.error)) {
|
|
6322
|
+
const pending = this._rpcPending.get(payload.id);
|
|
6323
|
+
if (pending) {
|
|
6324
|
+
clearTimeout(pending.timer);
|
|
6325
|
+
this._rpcPending.delete(payload.id);
|
|
6326
|
+
if (payload.error) {
|
|
6327
|
+
pending.reject(new Error(payload.error.message || "RPC error"));
|
|
6328
|
+
} else {
|
|
6329
|
+
pending.resolve(payload.result);
|
|
6330
|
+
}
|
|
6331
|
+
}
|
|
6332
|
+
return;
|
|
6333
|
+
}
|
|
6334
|
+
if (payload.method && payload.id) {
|
|
6335
|
+
const handler = this._rpcHandlers.get(payload.method);
|
|
6336
|
+
if (!handler) {
|
|
6337
|
+
await this.send(msg.from, JSON.stringify({
|
|
6338
|
+
jsonrpc: "2.0",
|
|
6339
|
+
id: payload.id,
|
|
6340
|
+
error: { code: -32601, message: `Method not found: ${payload.method}` }
|
|
6341
|
+
}), { messageType: "rpc-response", threadId: payload.id });
|
|
6342
|
+
return;
|
|
6343
|
+
}
|
|
6344
|
+
try {
|
|
6345
|
+
const result = await handler(payload.params || {}, msg.from);
|
|
6346
|
+
await this.send(msg.from, JSON.stringify({
|
|
6347
|
+
jsonrpc: "2.0",
|
|
6348
|
+
id: payload.id,
|
|
6349
|
+
result
|
|
6350
|
+
}), { messageType: "rpc-response", threadId: payload.id });
|
|
6351
|
+
} catch (err) {
|
|
6352
|
+
await this.send(msg.from, JSON.stringify({
|
|
6353
|
+
jsonrpc: "2.0",
|
|
6354
|
+
id: payload.id,
|
|
6355
|
+
error: { code: -32e3, message: err.message || "Handler error" }
|
|
6356
|
+
}), { messageType: "rpc-response", threadId: payload.id });
|
|
6357
|
+
}
|
|
6358
|
+
}
|
|
6359
|
+
} catch {
|
|
6360
|
+
}
|
|
6361
|
+
}, { interval: 500, adaptive: false, heartbeat: false });
|
|
6362
|
+
}
|
|
6363
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6364
|
+
// P2P DIRECT MODE — Bypass Relay When Possible
|
|
6365
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6366
|
+
/**
|
|
6367
|
+
* Send a message directly to a peer's webhook endpoint, bypassing the relay entirely.
|
|
6368
|
+
* The relay never sees the message — true peer-to-peer encrypted delivery.
|
|
6369
|
+
*
|
|
6370
|
+
* Falls back to relay-based send if direct delivery fails.
|
|
6371
|
+
*
|
|
6372
|
+
* @example
|
|
6373
|
+
* ```ts
|
|
6374
|
+
* // Try direct first, fall back to relay
|
|
6375
|
+
* const result = await agent.sendDirect('did:voidly:peer', 'Hello P2P!');
|
|
6376
|
+
* console.log(result.direct); // true if delivered directly, false if via relay
|
|
6377
|
+
* ```
|
|
6378
|
+
*/
|
|
6379
|
+
async sendDirect(recipientDid, message, options = {}) {
|
|
6380
|
+
try {
|
|
6381
|
+
const profile = await this.getIdentity(recipientDid);
|
|
6382
|
+
if (profile) {
|
|
6383
|
+
const webhookRes = await this._timedFetch(
|
|
6384
|
+
`${this.baseUrl}/v1/agent/identity/${recipientDid}`,
|
|
6385
|
+
{ headers: { "X-Agent-Key": this.apiKey } }
|
|
6386
|
+
);
|
|
6387
|
+
if (webhookRes.ok) {
|
|
6388
|
+
const data = await webhookRes.json();
|
|
6389
|
+
const webhookUrl = data.webhook_url;
|
|
6390
|
+
if (webhookUrl) {
|
|
6391
|
+
const nonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.box.nonceLength);
|
|
6392
|
+
const recipientPubKey = (0, import_tweetnacl_util.decodeBase64)(profile.encryption_public_key);
|
|
6393
|
+
const plaintext = (0, import_tweetnacl_util.decodeUTF8)(message);
|
|
6394
|
+
const ciphertext = import_tweetnacl.default.box(plaintext, nonce, recipientPubKey, this.encryptionKeyPair.secretKey);
|
|
6395
|
+
const envelope = JSON.stringify({
|
|
6396
|
+
from: this.did,
|
|
6397
|
+
to: recipientDid,
|
|
6398
|
+
ciphertext: (0, import_tweetnacl_util.encodeBase64)(ciphertext),
|
|
6399
|
+
nonce: (0, import_tweetnacl_util.encodeBase64)(nonce),
|
|
6400
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6401
|
+
message_type: options.messageType || "text",
|
|
6402
|
+
thread_id: options.threadId
|
|
6403
|
+
});
|
|
6404
|
+
const signature = import_tweetnacl.default.sign.detached((0, import_tweetnacl_util.decodeUTF8)(envelope), this.signingKeyPair.secretKey);
|
|
6405
|
+
const directRes = await this._timedFetch(webhookUrl, {
|
|
6406
|
+
method: "POST",
|
|
6407
|
+
headers: {
|
|
6408
|
+
"Content-Type": "application/json",
|
|
6409
|
+
"X-Voidly-Signature": `sha256=${(0, import_tweetnacl_util.encodeBase64)(signature)}`,
|
|
6410
|
+
"X-Voidly-Sender": this.did
|
|
6411
|
+
},
|
|
6412
|
+
body: envelope
|
|
6413
|
+
});
|
|
6414
|
+
if (directRes.ok) {
|
|
6415
|
+
const now = /* @__PURE__ */ new Date();
|
|
6416
|
+
return {
|
|
6417
|
+
id: `direct-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
6418
|
+
from: this.did,
|
|
6419
|
+
to: recipientDid,
|
|
6420
|
+
timestamp: now.toISOString(),
|
|
6421
|
+
expiresAt: new Date(now.getTime() + 864e5).toISOString(),
|
|
6422
|
+
encrypted: true,
|
|
6423
|
+
clientSide: true,
|
|
6424
|
+
direct: true
|
|
6425
|
+
};
|
|
6426
|
+
}
|
|
6427
|
+
}
|
|
6428
|
+
}
|
|
6429
|
+
}
|
|
6430
|
+
} catch {
|
|
6431
|
+
}
|
|
6432
|
+
const result = await this.send(recipientDid, message, options);
|
|
6433
|
+
return { ...result, direct: false };
|
|
6434
|
+
}
|
|
6435
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6436
|
+
// COVER TRAFFIC — Noise Protocol for Traffic Analysis Resistance
|
|
6437
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6438
|
+
/**
|
|
6439
|
+
* Enable cover traffic — sends encrypted noise at random intervals.
|
|
6440
|
+
* Makes real messages indistinguishable from cover traffic for any observer
|
|
6441
|
+
* monitoring message timing and frequency.
|
|
6442
|
+
*
|
|
6443
|
+
* Cover messages are encrypted and padded identically to real messages.
|
|
6444
|
+
* The relay cannot distinguish them from real traffic.
|
|
6445
|
+
*
|
|
6446
|
+
* @example
|
|
6447
|
+
* ```ts
|
|
6448
|
+
* // Send noise every ~30s (randomized ±50%)
|
|
6449
|
+
* agent.enableCoverTraffic({ intervalMs: 30000 });
|
|
6450
|
+
*
|
|
6451
|
+
* // Stop cover traffic
|
|
6452
|
+
* agent.disableCoverTraffic();
|
|
6453
|
+
* ```
|
|
6454
|
+
*/
|
|
6455
|
+
enableCoverTraffic(options = {}) {
|
|
6456
|
+
this.disableCoverTraffic();
|
|
6457
|
+
const baseInterval = options.intervalMs || 3e4;
|
|
6458
|
+
const sendNoise = async () => {
|
|
6459
|
+
try {
|
|
6460
|
+
const noise = import_tweetnacl.default.randomBytes(128 + Math.floor(Math.random() * 384));
|
|
6461
|
+
await this.send(this.did, (0, import_tweetnacl_util.encodeBase64)(noise), {
|
|
6462
|
+
messageType: "ping",
|
|
6463
|
+
// use 'ping' type — indistinguishable in encrypted payload
|
|
6464
|
+
ttl: 60
|
|
6465
|
+
// short TTL — noise auto-expires
|
|
6466
|
+
});
|
|
6467
|
+
} catch {
|
|
6468
|
+
}
|
|
6469
|
+
};
|
|
6470
|
+
const scheduleNext = () => {
|
|
6471
|
+
const jitter = baseInterval * (0.5 + Math.random());
|
|
6472
|
+
this._coverTrafficTimer = setTimeout(async () => {
|
|
6473
|
+
await sendNoise();
|
|
6474
|
+
if (this._coverTrafficTimer !== null) scheduleNext();
|
|
6475
|
+
}, jitter);
|
|
6476
|
+
};
|
|
6477
|
+
scheduleNext();
|
|
6478
|
+
}
|
|
6479
|
+
/**
|
|
6480
|
+
* Disable cover traffic.
|
|
6481
|
+
*/
|
|
6482
|
+
disableCoverTraffic() {
|
|
6483
|
+
if (this._coverTrafficTimer !== null) {
|
|
6484
|
+
clearTimeout(this._coverTrafficTimer);
|
|
6485
|
+
this._coverTrafficTimer = null;
|
|
6486
|
+
}
|
|
6487
|
+
}
|
|
6488
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6489
|
+
// RESILIENT OPERATIONS — Fallback for All Operations
|
|
6490
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6491
|
+
/**
|
|
6492
|
+
* Fetch from primary relay with fallback to alternate relays.
|
|
6493
|
+
* Unlike _timedFetch which only hits one URL, this tries all known relays.
|
|
6494
|
+
* @internal
|
|
6495
|
+
*/
|
|
6496
|
+
async _resilientFetch(path, init) {
|
|
6497
|
+
const relays = [this.baseUrl, ...this.fallbackRelays];
|
|
6498
|
+
let lastError = null;
|
|
6499
|
+
for (const relay of relays) {
|
|
6500
|
+
try {
|
|
6501
|
+
const res = await this._timedFetch(`${relay}${path}`, init);
|
|
6502
|
+
if (res.ok || res.status >= 400 && res.status < 500) return res;
|
|
6503
|
+
} catch (err) {
|
|
6504
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
6505
|
+
}
|
|
6506
|
+
}
|
|
6507
|
+
throw lastError || new Error("All relays failed");
|
|
6183
6508
|
}
|
|
6184
6509
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
6185
6510
|
// CONVERSATIONS — Thread Management
|
|
@@ -6318,13 +6643,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6318
6643
|
return {
|
|
6319
6644
|
relayCanSee: [
|
|
6320
6645
|
"Your DID (public identifier)",
|
|
6321
|
-
|
|
6646
|
+
"Recipient DIDs (relay needs them for routing)",
|
|
6322
6647
|
...this.jitterMs > 0 ? ["Approximate timing (jittered timestamps)"] : ["When you message (timestamps)"],
|
|
6323
|
-
"Message types
|
|
6324
|
-
"Thread structure (which messages are replies)",
|
|
6648
|
+
...this.sealedSender ? [] : ["Message types, thread IDs, content types (sent in cleartext without sealed sender)"],
|
|
6325
6649
|
"Channel membership (but NOT channel message content with client-side encryption)",
|
|
6326
6650
|
"Capability registrations",
|
|
6327
|
-
"Online/offline status",
|
|
6328
6651
|
"Approximate message size (even with padding, bounded to power-of-2)"
|
|
6329
6652
|
],
|
|
6330
6653
|
relayCannotSee: [
|
|
@@ -6334,7 +6657,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6334
6657
|
"Past message keys (forward secrecy \u2014 hash ratchet + DH ratchet, old keys deleted)",
|
|
6335
6658
|
"Future message keys (post-compromise recovery via DH ratchet \u2014 compromise heals after one round-trip)",
|
|
6336
6659
|
"Channel message content (client-side nacl.secretbox encryption \u2014 relay stores only ciphertext)",
|
|
6337
|
-
...this.sealedSender ? [
|
|
6660
|
+
...this.sealedSender ? [
|
|
6661
|
+
'Sender identity (sealed inside ciphertext \u2014 relay stores "sealed" not your DID)',
|
|
6662
|
+
"Message types, thread IDs, reply chains (packed inside ciphertext in v3)",
|
|
6663
|
+
"Message count (not incremented for sealed senders)"
|
|
6664
|
+
] : [],
|
|
6338
6665
|
...this.deniable ? ["Who authored a message (HMAC is symmetric \u2014 either party could have produced it)"] : []
|
|
6339
6666
|
],
|
|
6340
6667
|
protections: [
|
|
@@ -6352,22 +6679,32 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6352
6679
|
"Request timeouts (AbortController on all HTTP, configurable)",
|
|
6353
6680
|
"Request validation (fromCredentials validates key sizes and format)",
|
|
6354
6681
|
...this.paddingEnabled ? ["Message padding to power-of-2 boundary (traffic analysis resistance)"] : [],
|
|
6355
|
-
...this.sealedSender ? [
|
|
6682
|
+
...this.sealedSender ? [
|
|
6683
|
+
"Sealed sender (relay cannot see who sent a message)",
|
|
6684
|
+
"Metadata privacy (v3 \u2014 thread_id, message_type, reply_to packed inside ciphertext, stripped from relay storage)"
|
|
6685
|
+
] : [],
|
|
6356
6686
|
...this.requireSignatures ? ["Strict signature enforcement (reject unsigned/invalid messages)"] : [],
|
|
6357
|
-
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays)`] : [],
|
|
6687
|
+
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays \u2014 receive, discover, identity all use fallbacks)`] : [],
|
|
6358
6688
|
...this.jitterMs > 0 ? [`Timing jitter (random ${this.jitterMs}ms delay \u2014 metadata timing protection)`] : [],
|
|
6359
6689
|
...this.longPoll ? ["Long-poll transport (25s server-held connection \u2014 near-real-time delivery)"] : [],
|
|
6690
|
+
...this._coverTrafficTimer !== null ? ["Cover traffic (encrypted noise at random intervals \u2014 traffic analysis resistance)"] : [],
|
|
6691
|
+
"Agent RPC (invoke/onInvoke \u2014 synchronous function calls between agents)",
|
|
6692
|
+
"P2P direct send (bypass relay via webhook \u2014 true peer-to-peer when possible)",
|
|
6693
|
+
"Resilient operations (receive, discover, identity \u2014 all try fallback relays)",
|
|
6360
6694
|
"Auto-retry with exponential backoff",
|
|
6361
6695
|
"Offline message queue",
|
|
6362
6696
|
"did:key interoperability (W3C standard DID format)"
|
|
6363
6697
|
],
|
|
6364
6698
|
gaps: [
|
|
6365
6699
|
...!this.postQuantum || !this.mlkemPublicKey ? ["No post-quantum protection \u2014 enable postQuantum option and re-register"] : [],
|
|
6366
|
-
"
|
|
6700
|
+
...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
|
+
"Relay sees channel membership, task delegation, trust scores (social graph)",
|
|
6702
|
+
...this.fallbackRelays.length === 0 ? ["Single relay with no fallbacks \u2014 configure fallbackRelays for resilience"] : [],
|
|
6367
6703
|
"Ratchet state is in-memory (lost on process restart \u2014 export credentials to persist)",
|
|
6368
6704
|
...!this.deniable ? ["Ed25519 signatures are non-repudiable \u2014 enable deniable option for HMAC auth"] : [],
|
|
6369
6705
|
...!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"] : []
|
|
6706
|
+
...this.jitterMs === 0 ? ["No timing jitter \u2014 enable jitterMs option for metadata protection"] : [],
|
|
6707
|
+
...this._coverTrafficTimer === null ? ["No cover traffic \u2014 call enableCoverTraffic() to resist traffic analysis"] : []
|
|
6371
6708
|
]
|
|
6372
6709
|
};
|
|
6373
6710
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -3972,19 +3972,29 @@ async function ratchetStep(chainKey) {
|
|
|
3972
3972
|
);
|
|
3973
3973
|
return { nextChainKey, messageKey };
|
|
3974
3974
|
}
|
|
3975
|
-
function sealEnvelope(senderDid, plaintext) {
|
|
3976
|
-
|
|
3977
|
-
v:
|
|
3975
|
+
function sealEnvelope(senderDid, plaintext, meta) {
|
|
3976
|
+
const obj = {
|
|
3977
|
+
v: 3,
|
|
3978
3978
|
from: senderDid,
|
|
3979
3979
|
msg: plaintext,
|
|
3980
3980
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
3981
|
-
}
|
|
3981
|
+
};
|
|
3982
|
+
if (meta?.contentType && meta.contentType !== "text/plain") obj.ct = meta.contentType;
|
|
3983
|
+
if (meta?.messageType && meta.messageType !== "text") obj.mt = meta.messageType;
|
|
3984
|
+
if (meta?.threadId) obj.tid = meta.threadId;
|
|
3985
|
+
if (meta?.replyTo) obj.rto = meta.replyTo;
|
|
3986
|
+
return JSON.stringify(obj);
|
|
3982
3987
|
}
|
|
3983
3988
|
function unsealEnvelope(plaintext) {
|
|
3984
3989
|
try {
|
|
3985
3990
|
const parsed = JSON.parse(plaintext);
|
|
3986
|
-
if (parsed.v === 2 && parsed.from && parsed.msg) {
|
|
3987
|
-
|
|
3991
|
+
if ((parsed.v === 2 || parsed.v === 3) && parsed.from && parsed.msg) {
|
|
3992
|
+
const result = { from: parsed.from, msg: parsed.msg, ts: parsed.ts };
|
|
3993
|
+
if (parsed.ct) result.contentType = parsed.ct;
|
|
3994
|
+
if (parsed.mt) result.messageType = parsed.mt;
|
|
3995
|
+
if (parsed.tid) result.threadId = parsed.tid;
|
|
3996
|
+
if (parsed.rto) result.replyTo = parsed.rto;
|
|
3997
|
+
return result;
|
|
3988
3998
|
}
|
|
3989
3999
|
return null;
|
|
3990
4000
|
} catch {
|
|
@@ -4062,6 +4072,14 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4062
4072
|
this._identityCache = /* @__PURE__ */ new Map();
|
|
4063
4073
|
this._seenMessageIds = /* @__PURE__ */ new Set();
|
|
4064
4074
|
this._decryptFailCount = 0;
|
|
4075
|
+
// RPC handlers: method → handler function
|
|
4076
|
+
this._rpcHandlers = /* @__PURE__ */ new Map();
|
|
4077
|
+
// RPC pending responses: rpc_id → { resolve, reject, timer }
|
|
4078
|
+
this._rpcPending = /* @__PURE__ */ new Map();
|
|
4079
|
+
// Cover traffic state
|
|
4080
|
+
this._coverTrafficTimer = null;
|
|
4081
|
+
// RPC listener handle (started on first onInvoke)
|
|
4082
|
+
this._rpcListener = null;
|
|
4065
4083
|
this.did = identity.did;
|
|
4066
4084
|
this.apiKey = identity.apiKey;
|
|
4067
4085
|
this.signingKeyPair = identity.signingKeyPair;
|
|
@@ -4332,7 +4350,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4332
4350
|
const recipientPubKey = (0, import_tweetnacl_util.decodeBase64)(profile.encryption_public_key);
|
|
4333
4351
|
let plaintext = message;
|
|
4334
4352
|
if (useSealed) {
|
|
4335
|
-
plaintext = sealEnvelope(this.did, message
|
|
4353
|
+
plaintext = sealEnvelope(this.did, message, {
|
|
4354
|
+
contentType: options.contentType,
|
|
4355
|
+
messageType: options.messageType,
|
|
4356
|
+
threadId: options.threadId,
|
|
4357
|
+
replyTo: options.replyTo
|
|
4358
|
+
});
|
|
4336
4359
|
}
|
|
4337
4360
|
let contentBytes;
|
|
4338
4361
|
if (usePadding) {
|
|
@@ -4447,12 +4470,14 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4447
4470
|
nonce: (0, import_tweetnacl_util.encodeBase64)(nonce),
|
|
4448
4471
|
signature: (0, import_tweetnacl_util.encodeBase64)(signature),
|
|
4449
4472
|
envelope: envelopeData,
|
|
4450
|
-
content_type: options.contentType || "text/plain",
|
|
4451
|
-
message_type: options.messageType || "text",
|
|
4452
|
-
thread_id: options.threadId,
|
|
4453
|
-
reply_to: options.replyTo,
|
|
4454
4473
|
ttl: options.ttl
|
|
4455
4474
|
};
|
|
4475
|
+
if (!useSealed) {
|
|
4476
|
+
payload.content_type = options.contentType || "text/plain";
|
|
4477
|
+
payload.message_type = options.messageType || "text";
|
|
4478
|
+
payload.thread_id = options.threadId;
|
|
4479
|
+
payload.reply_to = options.replyTo;
|
|
4480
|
+
}
|
|
4456
4481
|
const relays = [this.baseUrl, ...this.fallbackRelays];
|
|
4457
4482
|
let lastError = null;
|
|
4458
4483
|
for (const relay of relays) {
|
|
@@ -4503,7 +4528,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4503
4528
|
if (options.contentType) params.set("content_type", options.contentType);
|
|
4504
4529
|
if (options.messageType) params.set("message_type", options.messageType);
|
|
4505
4530
|
if (options.unreadOnly) params.set("unread", "true");
|
|
4506
|
-
const res = await this.
|
|
4531
|
+
const res = await this._resilientFetch(`/v1/agent/receive/raw?${params}`, {
|
|
4507
4532
|
headers: { "X-Agent-Key": this.apiKey }
|
|
4508
4533
|
});
|
|
4509
4534
|
if (!res.ok) {
|
|
@@ -4515,7 +4540,20 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4515
4540
|
for (const msg of data.messages) {
|
|
4516
4541
|
try {
|
|
4517
4542
|
if (this._seenMessageIds.has(msg.id)) continue;
|
|
4518
|
-
|
|
4543
|
+
let senderEncPub;
|
|
4544
|
+
let senderSignPubBytes = null;
|
|
4545
|
+
if (msg.sender_encryption_key) {
|
|
4546
|
+
senderEncPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_encryption_key);
|
|
4547
|
+
if (msg.sender_signing_key) senderSignPubBytes = (0, import_tweetnacl_util.decodeBase64)(msg.sender_signing_key);
|
|
4548
|
+
} else if (msg.envelope) {
|
|
4549
|
+
const env = JSON.parse(msg.envelope);
|
|
4550
|
+
const senderProfile = await this.getIdentity(env.from);
|
|
4551
|
+
if (!senderProfile) continue;
|
|
4552
|
+
senderEncPub = (0, import_tweetnacl_util.decodeBase64)(senderProfile.encryption_public_key);
|
|
4553
|
+
if (senderProfile.signing_public_key) senderSignPubBytes = (0, import_tweetnacl_util.decodeBase64)(senderProfile.signing_public_key);
|
|
4554
|
+
} else {
|
|
4555
|
+
continue;
|
|
4556
|
+
}
|
|
4519
4557
|
const ciphertext = (0, import_tweetnacl_util.decodeBase64)(msg.ciphertext);
|
|
4520
4558
|
const nonce = (0, import_tweetnacl_util.decodeBase64)(msg.nonce);
|
|
4521
4559
|
let rawPlaintext = null;
|
|
@@ -4683,11 +4721,19 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4683
4721
|
}
|
|
4684
4722
|
let content = (0, import_tweetnacl_util.encodeUTF8)(plaintextBytes);
|
|
4685
4723
|
let senderDid = msg.from;
|
|
4724
|
+
let innerContentType;
|
|
4725
|
+
let innerMessageType;
|
|
4726
|
+
let innerThreadId;
|
|
4727
|
+
let innerReplyTo;
|
|
4686
4728
|
if (wasSealed || !proto) {
|
|
4687
4729
|
const unsealed = unsealEnvelope(content);
|
|
4688
4730
|
if (unsealed) {
|
|
4689
4731
|
content = unsealed.msg;
|
|
4690
4732
|
senderDid = unsealed.from;
|
|
4733
|
+
innerContentType = unsealed.contentType;
|
|
4734
|
+
innerMessageType = unsealed.messageType;
|
|
4735
|
+
innerThreadId = unsealed.threadId;
|
|
4736
|
+
innerReplyTo = unsealed.replyTo;
|
|
4691
4737
|
}
|
|
4692
4738
|
}
|
|
4693
4739
|
let signatureValid = false;
|
|
@@ -4708,12 +4754,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4708
4754
|
for (let i = 0; i < expectedHmac.length; i++) diff |= expectedHmac[i] ^ signatureBytes[i];
|
|
4709
4755
|
signatureValid = diff === 0;
|
|
4710
4756
|
}
|
|
4711
|
-
} else {
|
|
4712
|
-
const senderSignPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_signing_key);
|
|
4757
|
+
} else if (senderSignPubBytes) {
|
|
4713
4758
|
signatureValid = import_tweetnacl.default.sign.detached.verify(
|
|
4714
4759
|
(0, import_tweetnacl_util.decodeUTF8)(envelopeStr),
|
|
4715
4760
|
signatureBytes,
|
|
4716
|
-
|
|
4761
|
+
senderSignPubBytes
|
|
4717
4762
|
);
|
|
4718
4763
|
}
|
|
4719
4764
|
} catch {
|
|
@@ -4733,10 +4778,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4733
4778
|
from: senderDid,
|
|
4734
4779
|
to: msg.to,
|
|
4735
4780
|
content,
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4781
|
+
// v3: prefer metadata from inside ciphertext (relay can't see it)
|
|
4782
|
+
contentType: innerContentType || msg.content_type || "text/plain",
|
|
4783
|
+
messageType: innerMessageType || msg.message_type || "text",
|
|
4784
|
+
threadId: innerThreadId || msg.thread_id || null,
|
|
4785
|
+
replyTo: innerReplyTo || msg.reply_to || null,
|
|
4740
4786
|
signatureValid,
|
|
4741
4787
|
timestamp: msg.timestamp,
|
|
4742
4788
|
expiresAt: msg.expires_at
|
|
@@ -4797,7 +4843,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4797
4843
|
if (cached && Date.now() - cached.cachedAt < 3e5) {
|
|
4798
4844
|
return cached.profile;
|
|
4799
4845
|
}
|
|
4800
|
-
const res = await this.
|
|
4846
|
+
const res = await this._resilientFetch(`/v1/agent/identity/${did}`);
|
|
4801
4847
|
if (!res.ok) return null;
|
|
4802
4848
|
const profile = await res.json();
|
|
4803
4849
|
this._identityCache.set(did, { profile, cachedAt: Date.now() });
|
|
@@ -4815,7 +4861,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4815
4861
|
if (options.query) params.set("query", options.query);
|
|
4816
4862
|
if (options.capability) params.set("capability", options.capability);
|
|
4817
4863
|
if (options.limit) params.set("limit", String(options.limit));
|
|
4818
|
-
const res = await this.
|
|
4864
|
+
const res = await this._resilientFetch(`/v1/agent/discover?${params}`);
|
|
4819
4865
|
if (!res.ok) return [];
|
|
4820
4866
|
const data = await res.json();
|
|
4821
4867
|
return data.agents;
|
|
@@ -6169,6 +6215,285 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6169
6215
|
listener.stop();
|
|
6170
6216
|
}
|
|
6171
6217
|
this._listeners.clear();
|
|
6218
|
+
if (this._rpcListener) {
|
|
6219
|
+
this._rpcListener.stop();
|
|
6220
|
+
this._rpcListener = null;
|
|
6221
|
+
}
|
|
6222
|
+
for (const [id, pending] of this._rpcPending) {
|
|
6223
|
+
clearTimeout(pending.timer);
|
|
6224
|
+
pending.reject(new Error("Agent stopped"));
|
|
6225
|
+
}
|
|
6226
|
+
this._rpcPending.clear();
|
|
6227
|
+
this.disableCoverTraffic();
|
|
6228
|
+
}
|
|
6229
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6230
|
+
// AGENT RPC — Synchronous Function Invocation Between Agents
|
|
6231
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6232
|
+
/**
|
|
6233
|
+
* Invoke a function on a remote agent. Synchronous RPC over encrypted messaging.
|
|
6234
|
+
* The remote agent must have registered a handler via `onInvoke()`.
|
|
6235
|
+
*
|
|
6236
|
+
* @example
|
|
6237
|
+
* ```ts
|
|
6238
|
+
* // Call a translator agent
|
|
6239
|
+
* const result = await agent.invoke('did:voidly:translator', 'translate', {
|
|
6240
|
+
* text: 'Hello, world!',
|
|
6241
|
+
* to: 'ja',
|
|
6242
|
+
* });
|
|
6243
|
+
* console.log(result.translation); // こんにちは
|
|
6244
|
+
*
|
|
6245
|
+
* // With timeout
|
|
6246
|
+
* const data = await agent.invoke(peerDid, 'analyze', { url: '...' }, 15000);
|
|
6247
|
+
* ```
|
|
6248
|
+
*/
|
|
6249
|
+
async invoke(targetDid, method, params = {}, timeoutMs = 3e4) {
|
|
6250
|
+
const rpcId = `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
6251
|
+
return new Promise(async (resolve, reject) => {
|
|
6252
|
+
const timer = setTimeout(() => {
|
|
6253
|
+
this._rpcPending.delete(rpcId);
|
|
6254
|
+
reject(new Error(`RPC timeout: ${method}@${targetDid} after ${timeoutMs}ms`));
|
|
6255
|
+
}, timeoutMs);
|
|
6256
|
+
this._rpcPending.set(rpcId, { resolve, reject, timer });
|
|
6257
|
+
try {
|
|
6258
|
+
await this.send(targetDid, JSON.stringify({
|
|
6259
|
+
jsonrpc: "2.0",
|
|
6260
|
+
method,
|
|
6261
|
+
params,
|
|
6262
|
+
id: rpcId
|
|
6263
|
+
}), { messageType: "rpc-request", threadId: rpcId });
|
|
6264
|
+
} catch (err) {
|
|
6265
|
+
clearTimeout(timer);
|
|
6266
|
+
this._rpcPending.delete(rpcId);
|
|
6267
|
+
reject(err);
|
|
6268
|
+
}
|
|
6269
|
+
});
|
|
6270
|
+
}
|
|
6271
|
+
/**
|
|
6272
|
+
* Register a handler for incoming RPC invocations.
|
|
6273
|
+
* When another agent calls `invoke(yourDid, method, params)`, your handler runs.
|
|
6274
|
+
*
|
|
6275
|
+
* @example
|
|
6276
|
+
* ```ts
|
|
6277
|
+
* // Register a translation capability
|
|
6278
|
+
* agent.onInvoke('translate', async (params, callerDid) => {
|
|
6279
|
+
* const result = await myTranslateFunction(params.text, params.to);
|
|
6280
|
+
* return { translation: result };
|
|
6281
|
+
* });
|
|
6282
|
+
*
|
|
6283
|
+
* // Register a search capability
|
|
6284
|
+
* agent.onInvoke('search', async (params) => {
|
|
6285
|
+
* return { results: await searchDatabase(params.query) };
|
|
6286
|
+
* });
|
|
6287
|
+
* ```
|
|
6288
|
+
*/
|
|
6289
|
+
onInvoke(method, handler) {
|
|
6290
|
+
this._rpcHandlers.set(method, handler);
|
|
6291
|
+
this._ensureRpcListener();
|
|
6292
|
+
}
|
|
6293
|
+
/**
|
|
6294
|
+
* Remove an RPC handler.
|
|
6295
|
+
*/
|
|
6296
|
+
offInvoke(method) {
|
|
6297
|
+
this._rpcHandlers.delete(method);
|
|
6298
|
+
if (this._rpcHandlers.size === 0 && this._rpcListener) {
|
|
6299
|
+
this._rpcListener.stop();
|
|
6300
|
+
this._rpcListener = null;
|
|
6301
|
+
}
|
|
6302
|
+
}
|
|
6303
|
+
/** @internal Start listening for RPC requests and responses */
|
|
6304
|
+
_ensureRpcListener() {
|
|
6305
|
+
if (this._rpcListener) return;
|
|
6306
|
+
this._rpcListener = this.listen(async (msg) => {
|
|
6307
|
+
try {
|
|
6308
|
+
const payload = JSON.parse(msg.content);
|
|
6309
|
+
if (payload.jsonrpc !== "2.0") return;
|
|
6310
|
+
if (payload.id && (payload.result !== void 0 || payload.error)) {
|
|
6311
|
+
const pending = this._rpcPending.get(payload.id);
|
|
6312
|
+
if (pending) {
|
|
6313
|
+
clearTimeout(pending.timer);
|
|
6314
|
+
this._rpcPending.delete(payload.id);
|
|
6315
|
+
if (payload.error) {
|
|
6316
|
+
pending.reject(new Error(payload.error.message || "RPC error"));
|
|
6317
|
+
} else {
|
|
6318
|
+
pending.resolve(payload.result);
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6321
|
+
return;
|
|
6322
|
+
}
|
|
6323
|
+
if (payload.method && payload.id) {
|
|
6324
|
+
const handler = this._rpcHandlers.get(payload.method);
|
|
6325
|
+
if (!handler) {
|
|
6326
|
+
await this.send(msg.from, JSON.stringify({
|
|
6327
|
+
jsonrpc: "2.0",
|
|
6328
|
+
id: payload.id,
|
|
6329
|
+
error: { code: -32601, message: `Method not found: ${payload.method}` }
|
|
6330
|
+
}), { messageType: "rpc-response", threadId: payload.id });
|
|
6331
|
+
return;
|
|
6332
|
+
}
|
|
6333
|
+
try {
|
|
6334
|
+
const result = await handler(payload.params || {}, msg.from);
|
|
6335
|
+
await this.send(msg.from, JSON.stringify({
|
|
6336
|
+
jsonrpc: "2.0",
|
|
6337
|
+
id: payload.id,
|
|
6338
|
+
result
|
|
6339
|
+
}), { messageType: "rpc-response", threadId: payload.id });
|
|
6340
|
+
} catch (err) {
|
|
6341
|
+
await this.send(msg.from, JSON.stringify({
|
|
6342
|
+
jsonrpc: "2.0",
|
|
6343
|
+
id: payload.id,
|
|
6344
|
+
error: { code: -32e3, message: err.message || "Handler error" }
|
|
6345
|
+
}), { messageType: "rpc-response", threadId: payload.id });
|
|
6346
|
+
}
|
|
6347
|
+
}
|
|
6348
|
+
} catch {
|
|
6349
|
+
}
|
|
6350
|
+
}, { interval: 500, adaptive: false, heartbeat: false });
|
|
6351
|
+
}
|
|
6352
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6353
|
+
// P2P DIRECT MODE — Bypass Relay When Possible
|
|
6354
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6355
|
+
/**
|
|
6356
|
+
* Send a message directly to a peer's webhook endpoint, bypassing the relay entirely.
|
|
6357
|
+
* The relay never sees the message — true peer-to-peer encrypted delivery.
|
|
6358
|
+
*
|
|
6359
|
+
* Falls back to relay-based send if direct delivery fails.
|
|
6360
|
+
*
|
|
6361
|
+
* @example
|
|
6362
|
+
* ```ts
|
|
6363
|
+
* // Try direct first, fall back to relay
|
|
6364
|
+
* const result = await agent.sendDirect('did:voidly:peer', 'Hello P2P!');
|
|
6365
|
+
* console.log(result.direct); // true if delivered directly, false if via relay
|
|
6366
|
+
* ```
|
|
6367
|
+
*/
|
|
6368
|
+
async sendDirect(recipientDid, message, options = {}) {
|
|
6369
|
+
try {
|
|
6370
|
+
const profile = await this.getIdentity(recipientDid);
|
|
6371
|
+
if (profile) {
|
|
6372
|
+
const webhookRes = await this._timedFetch(
|
|
6373
|
+
`${this.baseUrl}/v1/agent/identity/${recipientDid}`,
|
|
6374
|
+
{ headers: { "X-Agent-Key": this.apiKey } }
|
|
6375
|
+
);
|
|
6376
|
+
if (webhookRes.ok) {
|
|
6377
|
+
const data = await webhookRes.json();
|
|
6378
|
+
const webhookUrl = data.webhook_url;
|
|
6379
|
+
if (webhookUrl) {
|
|
6380
|
+
const nonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.box.nonceLength);
|
|
6381
|
+
const recipientPubKey = (0, import_tweetnacl_util.decodeBase64)(profile.encryption_public_key);
|
|
6382
|
+
const plaintext = (0, import_tweetnacl_util.decodeUTF8)(message);
|
|
6383
|
+
const ciphertext = import_tweetnacl.default.box(plaintext, nonce, recipientPubKey, this.encryptionKeyPair.secretKey);
|
|
6384
|
+
const envelope = JSON.stringify({
|
|
6385
|
+
from: this.did,
|
|
6386
|
+
to: recipientDid,
|
|
6387
|
+
ciphertext: (0, import_tweetnacl_util.encodeBase64)(ciphertext),
|
|
6388
|
+
nonce: (0, import_tweetnacl_util.encodeBase64)(nonce),
|
|
6389
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6390
|
+
message_type: options.messageType || "text",
|
|
6391
|
+
thread_id: options.threadId
|
|
6392
|
+
});
|
|
6393
|
+
const signature = import_tweetnacl.default.sign.detached((0, import_tweetnacl_util.decodeUTF8)(envelope), this.signingKeyPair.secretKey);
|
|
6394
|
+
const directRes = await this._timedFetch(webhookUrl, {
|
|
6395
|
+
method: "POST",
|
|
6396
|
+
headers: {
|
|
6397
|
+
"Content-Type": "application/json",
|
|
6398
|
+
"X-Voidly-Signature": `sha256=${(0, import_tweetnacl_util.encodeBase64)(signature)}`,
|
|
6399
|
+
"X-Voidly-Sender": this.did
|
|
6400
|
+
},
|
|
6401
|
+
body: envelope
|
|
6402
|
+
});
|
|
6403
|
+
if (directRes.ok) {
|
|
6404
|
+
const now = /* @__PURE__ */ new Date();
|
|
6405
|
+
return {
|
|
6406
|
+
id: `direct-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
6407
|
+
from: this.did,
|
|
6408
|
+
to: recipientDid,
|
|
6409
|
+
timestamp: now.toISOString(),
|
|
6410
|
+
expiresAt: new Date(now.getTime() + 864e5).toISOString(),
|
|
6411
|
+
encrypted: true,
|
|
6412
|
+
clientSide: true,
|
|
6413
|
+
direct: true
|
|
6414
|
+
};
|
|
6415
|
+
}
|
|
6416
|
+
}
|
|
6417
|
+
}
|
|
6418
|
+
}
|
|
6419
|
+
} catch {
|
|
6420
|
+
}
|
|
6421
|
+
const result = await this.send(recipientDid, message, options);
|
|
6422
|
+
return { ...result, direct: false };
|
|
6423
|
+
}
|
|
6424
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6425
|
+
// COVER TRAFFIC — Noise Protocol for Traffic Analysis Resistance
|
|
6426
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6427
|
+
/**
|
|
6428
|
+
* Enable cover traffic — sends encrypted noise at random intervals.
|
|
6429
|
+
* Makes real messages indistinguishable from cover traffic for any observer
|
|
6430
|
+
* monitoring message timing and frequency.
|
|
6431
|
+
*
|
|
6432
|
+
* Cover messages are encrypted and padded identically to real messages.
|
|
6433
|
+
* The relay cannot distinguish them from real traffic.
|
|
6434
|
+
*
|
|
6435
|
+
* @example
|
|
6436
|
+
* ```ts
|
|
6437
|
+
* // Send noise every ~30s (randomized ±50%)
|
|
6438
|
+
* agent.enableCoverTraffic({ intervalMs: 30000 });
|
|
6439
|
+
*
|
|
6440
|
+
* // Stop cover traffic
|
|
6441
|
+
* agent.disableCoverTraffic();
|
|
6442
|
+
* ```
|
|
6443
|
+
*/
|
|
6444
|
+
enableCoverTraffic(options = {}) {
|
|
6445
|
+
this.disableCoverTraffic();
|
|
6446
|
+
const baseInterval = options.intervalMs || 3e4;
|
|
6447
|
+
const sendNoise = async () => {
|
|
6448
|
+
try {
|
|
6449
|
+
const noise = import_tweetnacl.default.randomBytes(128 + Math.floor(Math.random() * 384));
|
|
6450
|
+
await this.send(this.did, (0, import_tweetnacl_util.encodeBase64)(noise), {
|
|
6451
|
+
messageType: "ping",
|
|
6452
|
+
// use 'ping' type — indistinguishable in encrypted payload
|
|
6453
|
+
ttl: 60
|
|
6454
|
+
// short TTL — noise auto-expires
|
|
6455
|
+
});
|
|
6456
|
+
} catch {
|
|
6457
|
+
}
|
|
6458
|
+
};
|
|
6459
|
+
const scheduleNext = () => {
|
|
6460
|
+
const jitter = baseInterval * (0.5 + Math.random());
|
|
6461
|
+
this._coverTrafficTimer = setTimeout(async () => {
|
|
6462
|
+
await sendNoise();
|
|
6463
|
+
if (this._coverTrafficTimer !== null) scheduleNext();
|
|
6464
|
+
}, jitter);
|
|
6465
|
+
};
|
|
6466
|
+
scheduleNext();
|
|
6467
|
+
}
|
|
6468
|
+
/**
|
|
6469
|
+
* Disable cover traffic.
|
|
6470
|
+
*/
|
|
6471
|
+
disableCoverTraffic() {
|
|
6472
|
+
if (this._coverTrafficTimer !== null) {
|
|
6473
|
+
clearTimeout(this._coverTrafficTimer);
|
|
6474
|
+
this._coverTrafficTimer = null;
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
6477
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6478
|
+
// RESILIENT OPERATIONS — Fallback for All Operations
|
|
6479
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6480
|
+
/**
|
|
6481
|
+
* Fetch from primary relay with fallback to alternate relays.
|
|
6482
|
+
* Unlike _timedFetch which only hits one URL, this tries all known relays.
|
|
6483
|
+
* @internal
|
|
6484
|
+
*/
|
|
6485
|
+
async _resilientFetch(path, init) {
|
|
6486
|
+
const relays = [this.baseUrl, ...this.fallbackRelays];
|
|
6487
|
+
let lastError = null;
|
|
6488
|
+
for (const relay of relays) {
|
|
6489
|
+
try {
|
|
6490
|
+
const res = await this._timedFetch(`${relay}${path}`, init);
|
|
6491
|
+
if (res.ok || res.status >= 400 && res.status < 500) return res;
|
|
6492
|
+
} catch (err) {
|
|
6493
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
6494
|
+
}
|
|
6495
|
+
}
|
|
6496
|
+
throw lastError || new Error("All relays failed");
|
|
6172
6497
|
}
|
|
6173
6498
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
6174
6499
|
// CONVERSATIONS — Thread Management
|
|
@@ -6307,13 +6632,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6307
6632
|
return {
|
|
6308
6633
|
relayCanSee: [
|
|
6309
6634
|
"Your DID (public identifier)",
|
|
6310
|
-
|
|
6635
|
+
"Recipient DIDs (relay needs them for routing)",
|
|
6311
6636
|
...this.jitterMs > 0 ? ["Approximate timing (jittered timestamps)"] : ["When you message (timestamps)"],
|
|
6312
|
-
"Message types
|
|
6313
|
-
"Thread structure (which messages are replies)",
|
|
6637
|
+
...this.sealedSender ? [] : ["Message types, thread IDs, content types (sent in cleartext without sealed sender)"],
|
|
6314
6638
|
"Channel membership (but NOT channel message content with client-side encryption)",
|
|
6315
6639
|
"Capability registrations",
|
|
6316
|
-
"Online/offline status",
|
|
6317
6640
|
"Approximate message size (even with padding, bounded to power-of-2)"
|
|
6318
6641
|
],
|
|
6319
6642
|
relayCannotSee: [
|
|
@@ -6323,7 +6646,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6323
6646
|
"Past message keys (forward secrecy \u2014 hash ratchet + DH ratchet, old keys deleted)",
|
|
6324
6647
|
"Future message keys (post-compromise recovery via DH ratchet \u2014 compromise heals after one round-trip)",
|
|
6325
6648
|
"Channel message content (client-side nacl.secretbox encryption \u2014 relay stores only ciphertext)",
|
|
6326
|
-
...this.sealedSender ? [
|
|
6649
|
+
...this.sealedSender ? [
|
|
6650
|
+
'Sender identity (sealed inside ciphertext \u2014 relay stores "sealed" not your DID)',
|
|
6651
|
+
"Message types, thread IDs, reply chains (packed inside ciphertext in v3)",
|
|
6652
|
+
"Message count (not incremented for sealed senders)"
|
|
6653
|
+
] : [],
|
|
6327
6654
|
...this.deniable ? ["Who authored a message (HMAC is symmetric \u2014 either party could have produced it)"] : []
|
|
6328
6655
|
],
|
|
6329
6656
|
protections: [
|
|
@@ -6341,22 +6668,32 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
6341
6668
|
"Request timeouts (AbortController on all HTTP, configurable)",
|
|
6342
6669
|
"Request validation (fromCredentials validates key sizes and format)",
|
|
6343
6670
|
...this.paddingEnabled ? ["Message padding to power-of-2 boundary (traffic analysis resistance)"] : [],
|
|
6344
|
-
...this.sealedSender ? [
|
|
6671
|
+
...this.sealedSender ? [
|
|
6672
|
+
"Sealed sender (relay cannot see who sent a message)",
|
|
6673
|
+
"Metadata privacy (v3 \u2014 thread_id, message_type, reply_to packed inside ciphertext, stripped from relay storage)"
|
|
6674
|
+
] : [],
|
|
6345
6675
|
...this.requireSignatures ? ["Strict signature enforcement (reject unsigned/invalid messages)"] : [],
|
|
6346
|
-
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays)`] : [],
|
|
6676
|
+
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays \u2014 receive, discover, identity all use fallbacks)`] : [],
|
|
6347
6677
|
...this.jitterMs > 0 ? [`Timing jitter (random ${this.jitterMs}ms delay \u2014 metadata timing protection)`] : [],
|
|
6348
6678
|
...this.longPoll ? ["Long-poll transport (25s server-held connection \u2014 near-real-time delivery)"] : [],
|
|
6679
|
+
...this._coverTrafficTimer !== null ? ["Cover traffic (encrypted noise at random intervals \u2014 traffic analysis resistance)"] : [],
|
|
6680
|
+
"Agent RPC (invoke/onInvoke \u2014 synchronous function calls between agents)",
|
|
6681
|
+
"P2P direct send (bypass relay via webhook \u2014 true peer-to-peer when possible)",
|
|
6682
|
+
"Resilient operations (receive, discover, identity \u2014 all try fallback relays)",
|
|
6349
6683
|
"Auto-retry with exponential backoff",
|
|
6350
6684
|
"Offline message queue",
|
|
6351
6685
|
"did:key interoperability (W3C standard DID format)"
|
|
6352
6686
|
],
|
|
6353
6687
|
gaps: [
|
|
6354
6688
|
...!this.postQuantum || !this.mlkemPublicKey ? ["No post-quantum protection \u2014 enable postQuantum option and re-register"] : [],
|
|
6355
|
-
"
|
|
6689
|
+
...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
|
+
"Relay sees channel membership, task delegation, trust scores (social graph)",
|
|
6691
|
+
...this.fallbackRelays.length === 0 ? ["Single relay with no fallbacks \u2014 configure fallbackRelays for resilience"] : [],
|
|
6356
6692
|
"Ratchet state is in-memory (lost on process restart \u2014 export credentials to persist)",
|
|
6357
6693
|
...!this.deniable ? ["Ed25519 signatures are non-repudiable \u2014 enable deniable option for HMAC auth"] : [],
|
|
6358
6694
|
...!this.doubleRatchet ? ["Hash ratchet only \u2014 enable doubleRatchet option for post-compromise recovery"] : [],
|
|
6359
|
-
...this.jitterMs === 0 ? ["No timing jitter \u2014 enable jitterMs option for metadata protection"] : []
|
|
6695
|
+
...this.jitterMs === 0 ? ["No timing jitter \u2014 enable jitterMs option for metadata protection"] : [],
|
|
6696
|
+
...this._coverTrafficTimer === null ? ["No cover traffic \u2014 call enableCoverTraffic() to resist traffic analysis"] : []
|
|
6360
6697
|
]
|
|
6361
6698
|
};
|
|
6362
6699
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidly/agent-sdk",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "E2E encrypted agent-to-agent communication SDK — Double Ratchet, X3DH, deniable auth, ML-KEM-768 post-quantum,
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"description": "E2E encrypted agent-to-agent communication SDK — Double Ratchet, X3DH, deniable auth, ML-KEM-768 post-quantum, metadata privacy, RPC, P2P direct, cover traffic",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|