@voidly/agent-sdk 1.9.0 → 2.0.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 +45 -4
- package/dist/index.d.ts +45 -4
- package/dist/index.js +235 -28
- package/dist/index.mjs +235 -28
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -54,6 +54,12 @@ interface VoidlyAgentConfig {
|
|
|
54
54
|
autoPin?: boolean;
|
|
55
55
|
/** Default retry attempts for send() (default: 3) */
|
|
56
56
|
retries?: number;
|
|
57
|
+
/** Fallback relays — if primary fails, try these in order */
|
|
58
|
+
fallbackRelays?: string[];
|
|
59
|
+
/** Enable message padding to resist traffic analysis (default: true) */
|
|
60
|
+
padding?: boolean;
|
|
61
|
+
/** Enable sealed sender — hide sender DID from relay metadata (default: false) */
|
|
62
|
+
sealedSender?: boolean;
|
|
57
63
|
}
|
|
58
64
|
interface ListenOptions {
|
|
59
65
|
/** Milliseconds between polls (default: 2000, min: 500) */
|
|
@@ -128,9 +134,14 @@ declare class VoidlyAgent {
|
|
|
128
134
|
private baseUrl;
|
|
129
135
|
private autoPin;
|
|
130
136
|
private defaultRetries;
|
|
137
|
+
private fallbackRelays;
|
|
138
|
+
private paddingEnabled;
|
|
139
|
+
private sealedSender;
|
|
131
140
|
private _pinnedDids;
|
|
132
141
|
private _listeners;
|
|
133
142
|
private _conversations;
|
|
143
|
+
private _offlineQueue;
|
|
144
|
+
private _ratchetStates;
|
|
134
145
|
private constructor();
|
|
135
146
|
/**
|
|
136
147
|
* Register a new agent on the Voidly relay.
|
|
@@ -163,13 +174,17 @@ declare class VoidlyAgent {
|
|
|
163
174
|
encryptionPublicKey: string;
|
|
164
175
|
};
|
|
165
176
|
/**
|
|
166
|
-
* Send an E2E encrypted message with
|
|
177
|
+
* Send an E2E encrypted message with hardened security.
|
|
167
178
|
* Encryption happens locally — the relay NEVER sees plaintext or private keys.
|
|
168
179
|
*
|
|
169
|
-
*
|
|
180
|
+
* Security features:
|
|
181
|
+
* - **Message padding** — ciphertext padded to power-of-2 boundary (traffic analysis resistance)
|
|
182
|
+
* - **Hash ratchet** — per-conversation forward secrecy (compromise key[n] can't derive key[n-1])
|
|
183
|
+
* - **Sealed sender** — optionally hide sender DID from relay metadata
|
|
170
184
|
* - **Auto-retry** with exponential backoff on transient failures
|
|
171
|
-
* - **
|
|
172
|
-
* - **
|
|
185
|
+
* - **Multi-relay fallback** — try backup relays if primary is down
|
|
186
|
+
* - **Offline queue** — queue messages if all relays fail
|
|
187
|
+
* - **Transparent TOFU** — auto-pin recipient keys on first contact
|
|
173
188
|
*/
|
|
174
189
|
send(recipientDid: string, message: string, options?: {
|
|
175
190
|
contentType?: string;
|
|
@@ -181,6 +196,10 @@ declare class VoidlyAgent {
|
|
|
181
196
|
retries?: number;
|
|
182
197
|
/** Skip auto key pinning for this message */
|
|
183
198
|
skipPin?: boolean;
|
|
199
|
+
/** Force sealed sender for this message */
|
|
200
|
+
sealedSender?: boolean;
|
|
201
|
+
/** Disable padding for this message */
|
|
202
|
+
noPadding?: boolean;
|
|
184
203
|
}): Promise<SendResult>;
|
|
185
204
|
/**
|
|
186
205
|
* Receive and decrypt messages. Decryption happens locally.
|
|
@@ -842,6 +861,28 @@ declare class VoidlyAgent {
|
|
|
842
861
|
private _autoPinKeys;
|
|
843
862
|
/** @internal Fetch with exponential backoff retry */
|
|
844
863
|
private _fetchWithRetry;
|
|
864
|
+
/**
|
|
865
|
+
* Drain the offline message queue — retry sending queued messages.
|
|
866
|
+
* Call this when connectivity is restored.
|
|
867
|
+
* Returns: number of messages successfully sent.
|
|
868
|
+
*/
|
|
869
|
+
drainQueue(): Promise<{
|
|
870
|
+
sent: number;
|
|
871
|
+
failed: number;
|
|
872
|
+
remaining: number;
|
|
873
|
+
}>;
|
|
874
|
+
/** Number of messages waiting in the offline queue */
|
|
875
|
+
get queueLength(): number;
|
|
876
|
+
/**
|
|
877
|
+
* Returns what the relay can and cannot see about this agent.
|
|
878
|
+
* Call this to understand your threat model. Total transparency.
|
|
879
|
+
*/
|
|
880
|
+
threatModel(): {
|
|
881
|
+
relayCanSee: string[];
|
|
882
|
+
relayCannotSee: string[];
|
|
883
|
+
protections: string[];
|
|
884
|
+
gaps: string[];
|
|
885
|
+
};
|
|
845
886
|
}
|
|
846
887
|
/**
|
|
847
888
|
* A conversation between two agents.
|
package/dist/index.d.ts
CHANGED
|
@@ -54,6 +54,12 @@ interface VoidlyAgentConfig {
|
|
|
54
54
|
autoPin?: boolean;
|
|
55
55
|
/** Default retry attempts for send() (default: 3) */
|
|
56
56
|
retries?: number;
|
|
57
|
+
/** Fallback relays — if primary fails, try these in order */
|
|
58
|
+
fallbackRelays?: string[];
|
|
59
|
+
/** Enable message padding to resist traffic analysis (default: true) */
|
|
60
|
+
padding?: boolean;
|
|
61
|
+
/** Enable sealed sender — hide sender DID from relay metadata (default: false) */
|
|
62
|
+
sealedSender?: boolean;
|
|
57
63
|
}
|
|
58
64
|
interface ListenOptions {
|
|
59
65
|
/** Milliseconds between polls (default: 2000, min: 500) */
|
|
@@ -128,9 +134,14 @@ declare class VoidlyAgent {
|
|
|
128
134
|
private baseUrl;
|
|
129
135
|
private autoPin;
|
|
130
136
|
private defaultRetries;
|
|
137
|
+
private fallbackRelays;
|
|
138
|
+
private paddingEnabled;
|
|
139
|
+
private sealedSender;
|
|
131
140
|
private _pinnedDids;
|
|
132
141
|
private _listeners;
|
|
133
142
|
private _conversations;
|
|
143
|
+
private _offlineQueue;
|
|
144
|
+
private _ratchetStates;
|
|
134
145
|
private constructor();
|
|
135
146
|
/**
|
|
136
147
|
* Register a new agent on the Voidly relay.
|
|
@@ -163,13 +174,17 @@ declare class VoidlyAgent {
|
|
|
163
174
|
encryptionPublicKey: string;
|
|
164
175
|
};
|
|
165
176
|
/**
|
|
166
|
-
* Send an E2E encrypted message with
|
|
177
|
+
* Send an E2E encrypted message with hardened security.
|
|
167
178
|
* Encryption happens locally — the relay NEVER sees plaintext or private keys.
|
|
168
179
|
*
|
|
169
|
-
*
|
|
180
|
+
* Security features:
|
|
181
|
+
* - **Message padding** — ciphertext padded to power-of-2 boundary (traffic analysis resistance)
|
|
182
|
+
* - **Hash ratchet** — per-conversation forward secrecy (compromise key[n] can't derive key[n-1])
|
|
183
|
+
* - **Sealed sender** — optionally hide sender DID from relay metadata
|
|
170
184
|
* - **Auto-retry** with exponential backoff on transient failures
|
|
171
|
-
* - **
|
|
172
|
-
* - **
|
|
185
|
+
* - **Multi-relay fallback** — try backup relays if primary is down
|
|
186
|
+
* - **Offline queue** — queue messages if all relays fail
|
|
187
|
+
* - **Transparent TOFU** — auto-pin recipient keys on first contact
|
|
173
188
|
*/
|
|
174
189
|
send(recipientDid: string, message: string, options?: {
|
|
175
190
|
contentType?: string;
|
|
@@ -181,6 +196,10 @@ declare class VoidlyAgent {
|
|
|
181
196
|
retries?: number;
|
|
182
197
|
/** Skip auto key pinning for this message */
|
|
183
198
|
skipPin?: boolean;
|
|
199
|
+
/** Force sealed sender for this message */
|
|
200
|
+
sealedSender?: boolean;
|
|
201
|
+
/** Disable padding for this message */
|
|
202
|
+
noPadding?: boolean;
|
|
184
203
|
}): Promise<SendResult>;
|
|
185
204
|
/**
|
|
186
205
|
* Receive and decrypt messages. Decryption happens locally.
|
|
@@ -842,6 +861,28 @@ declare class VoidlyAgent {
|
|
|
842
861
|
private _autoPinKeys;
|
|
843
862
|
/** @internal Fetch with exponential backoff retry */
|
|
844
863
|
private _fetchWithRetry;
|
|
864
|
+
/**
|
|
865
|
+
* Drain the offline message queue — retry sending queued messages.
|
|
866
|
+
* Call this when connectivity is restored.
|
|
867
|
+
* Returns: number of messages successfully sent.
|
|
868
|
+
*/
|
|
869
|
+
drainQueue(): Promise<{
|
|
870
|
+
sent: number;
|
|
871
|
+
failed: number;
|
|
872
|
+
remaining: number;
|
|
873
|
+
}>;
|
|
874
|
+
/** Number of messages waiting in the offline queue */
|
|
875
|
+
get queueLength(): number;
|
|
876
|
+
/**
|
|
877
|
+
* Returns what the relay can and cannot see about this agent.
|
|
878
|
+
* Call this to understand your threat model. Total transparency.
|
|
879
|
+
*/
|
|
880
|
+
threatModel(): {
|
|
881
|
+
relayCanSee: string[];
|
|
882
|
+
relayCannotSee: string[];
|
|
883
|
+
protections: string[];
|
|
884
|
+
gaps: string[];
|
|
885
|
+
};
|
|
845
886
|
}
|
|
846
887
|
/**
|
|
847
888
|
* A conversation between two agents.
|
package/dist/index.js
CHANGED
|
@@ -2346,11 +2346,72 @@ async function sha256(data) {
|
|
|
2346
2346
|
const { createHash } = await import("crypto");
|
|
2347
2347
|
return createHash("sha256").update(data).digest("hex");
|
|
2348
2348
|
}
|
|
2349
|
+
function padMessage(content) {
|
|
2350
|
+
const contentLen = content.length;
|
|
2351
|
+
const totalLen = Math.max(256, nextPowerOf2(contentLen + 2));
|
|
2352
|
+
const padded = new Uint8Array(totalLen);
|
|
2353
|
+
padded[0] = contentLen >> 8 & 255;
|
|
2354
|
+
padded[1] = contentLen & 255;
|
|
2355
|
+
padded.set(content, 2);
|
|
2356
|
+
const randomPad = import_tweetnacl.default.randomBytes(totalLen - contentLen - 2);
|
|
2357
|
+
padded.set(randomPad, contentLen + 2);
|
|
2358
|
+
return padded;
|
|
2359
|
+
}
|
|
2360
|
+
function unpadMessage(padded) {
|
|
2361
|
+
if (padded.length < 2) return padded;
|
|
2362
|
+
const contentLen = padded[0] << 8 | padded[1];
|
|
2363
|
+
if (contentLen + 2 > padded.length) return padded;
|
|
2364
|
+
return padded.slice(2, 2 + contentLen);
|
|
2365
|
+
}
|
|
2366
|
+
function nextPowerOf2(n) {
|
|
2367
|
+
let p = 1;
|
|
2368
|
+
while (p < n) p <<= 1;
|
|
2369
|
+
return p;
|
|
2370
|
+
}
|
|
2371
|
+
async function ratchetStep(chainKey) {
|
|
2372
|
+
const encoder = new TextEncoder();
|
|
2373
|
+
if (typeof globalThis.crypto?.subtle !== "undefined") {
|
|
2374
|
+
const ckInput = new Uint8Array([...chainKey, 1]);
|
|
2375
|
+
const mkInput = new Uint8Array([...chainKey, 2]);
|
|
2376
|
+
const nextChainKey2 = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", ckInput));
|
|
2377
|
+
const messageKey2 = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", mkInput));
|
|
2378
|
+
return { nextChainKey: nextChainKey2, messageKey: messageKey2 };
|
|
2379
|
+
}
|
|
2380
|
+
const { createHash } = await import("crypto");
|
|
2381
|
+
const nextChainKey = new Uint8Array(
|
|
2382
|
+
createHash("sha256").update(Buffer.from([...chainKey, 1])).digest()
|
|
2383
|
+
);
|
|
2384
|
+
const messageKey = new Uint8Array(
|
|
2385
|
+
createHash("sha256").update(Buffer.from([...chainKey, 2])).digest()
|
|
2386
|
+
);
|
|
2387
|
+
return { nextChainKey, messageKey };
|
|
2388
|
+
}
|
|
2389
|
+
function sealEnvelope(senderDid, plaintext) {
|
|
2390
|
+
return JSON.stringify({
|
|
2391
|
+
v: 2,
|
|
2392
|
+
from: senderDid,
|
|
2393
|
+
msg: plaintext,
|
|
2394
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
function unsealEnvelope(plaintext) {
|
|
2398
|
+
try {
|
|
2399
|
+
const parsed = JSON.parse(plaintext);
|
|
2400
|
+
if (parsed.v === 2 && parsed.from && parsed.msg) {
|
|
2401
|
+
return { from: parsed.from, msg: parsed.msg, ts: parsed.ts };
|
|
2402
|
+
}
|
|
2403
|
+
return null;
|
|
2404
|
+
} catch {
|
|
2405
|
+
return null;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2349
2408
|
var VoidlyAgent = class _VoidlyAgent {
|
|
2350
2409
|
constructor(identity, config) {
|
|
2351
2410
|
this._pinnedDids = /* @__PURE__ */ new Set();
|
|
2352
2411
|
this._listeners = /* @__PURE__ */ new Set();
|
|
2353
2412
|
this._conversations = /* @__PURE__ */ new Map();
|
|
2413
|
+
this._offlineQueue = [];
|
|
2414
|
+
this._ratchetStates = /* @__PURE__ */ new Map();
|
|
2354
2415
|
this.did = identity.did;
|
|
2355
2416
|
this.apiKey = identity.apiKey;
|
|
2356
2417
|
this.signingKeyPair = identity.signingKeyPair;
|
|
@@ -2358,6 +2419,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2358
2419
|
this.baseUrl = config?.baseUrl || "https://api.voidly.ai";
|
|
2359
2420
|
this.autoPin = config?.autoPin !== false;
|
|
2360
2421
|
this.defaultRetries = config?.retries ?? 3;
|
|
2422
|
+
this.fallbackRelays = config?.fallbackRelays || [];
|
|
2423
|
+
this.paddingEnabled = config?.padding !== false;
|
|
2424
|
+
this.sealedSender = config?.sealedSender || false;
|
|
2361
2425
|
}
|
|
2362
2426
|
// ─── Factory Methods ────────────────────────────────────────────────────────
|
|
2363
2427
|
/**
|
|
@@ -2424,16 +2488,22 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2424
2488
|
}
|
|
2425
2489
|
// ─── Messaging ──────────────────────────────────────────────────────────────
|
|
2426
2490
|
/**
|
|
2427
|
-
* Send an E2E encrypted message with
|
|
2491
|
+
* Send an E2E encrypted message with hardened security.
|
|
2428
2492
|
* Encryption happens locally — the relay NEVER sees plaintext or private keys.
|
|
2429
2493
|
*
|
|
2430
|
-
*
|
|
2494
|
+
* Security features:
|
|
2495
|
+
* - **Message padding** — ciphertext padded to power-of-2 boundary (traffic analysis resistance)
|
|
2496
|
+
* - **Hash ratchet** — per-conversation forward secrecy (compromise key[n] can't derive key[n-1])
|
|
2497
|
+
* - **Sealed sender** — optionally hide sender DID from relay metadata
|
|
2431
2498
|
* - **Auto-retry** with exponential backoff on transient failures
|
|
2432
|
-
* - **
|
|
2433
|
-
* - **
|
|
2499
|
+
* - **Multi-relay fallback** — try backup relays if primary is down
|
|
2500
|
+
* - **Offline queue** — queue messages if all relays fail
|
|
2501
|
+
* - **Transparent TOFU** — auto-pin recipient keys on first contact
|
|
2434
2502
|
*/
|
|
2435
2503
|
async send(recipientDid, message, options = {}) {
|
|
2436
2504
|
const maxRetries = options.retries ?? this.defaultRetries;
|
|
2505
|
+
const usePadding = !options.noPadding && this.paddingEnabled;
|
|
2506
|
+
const useSealed = options.sealedSender ?? this.sealedSender;
|
|
2437
2507
|
const profile = await this.getIdentity(recipientDid);
|
|
2438
2508
|
if (!profile) {
|
|
2439
2509
|
throw new Error(`Recipient ${recipientDid} not found`);
|
|
@@ -2442,7 +2512,31 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2442
2512
|
await this._autoPinKeys(recipientDid);
|
|
2443
2513
|
}
|
|
2444
2514
|
const recipientPubKey = (0, import_tweetnacl_util.decodeBase64)(profile.encryption_public_key);
|
|
2445
|
-
|
|
2515
|
+
let plaintext = message;
|
|
2516
|
+
if (useSealed) {
|
|
2517
|
+
plaintext = sealEnvelope(this.did, message);
|
|
2518
|
+
}
|
|
2519
|
+
let messageBytes;
|
|
2520
|
+
if (usePadding) {
|
|
2521
|
+
messageBytes = padMessage((0, import_tweetnacl_util.decodeUTF8)(plaintext));
|
|
2522
|
+
} else {
|
|
2523
|
+
messageBytes = (0, import_tweetnacl_util.decodeUTF8)(plaintext);
|
|
2524
|
+
}
|
|
2525
|
+
const pairId = `${this.did}:${recipientDid}`;
|
|
2526
|
+
const ratchetState = this._ratchetStates.get(pairId);
|
|
2527
|
+
let ratchetStep_n = 0;
|
|
2528
|
+
if (ratchetState) {
|
|
2529
|
+
const { nextChainKey, messageKey } = await ratchetStep(ratchetState.chainKey);
|
|
2530
|
+
ratchetState.chainKey = nextChainKey;
|
|
2531
|
+
ratchetState.step++;
|
|
2532
|
+
ratchetStep_n = ratchetState.step;
|
|
2533
|
+
} else {
|
|
2534
|
+
const sharedSecret = import_tweetnacl.default.box.before(recipientPubKey, this.encryptionKeyPair.secretKey);
|
|
2535
|
+
this._ratchetStates.set(pairId, {
|
|
2536
|
+
chainKey: sharedSecret,
|
|
2537
|
+
step: 0
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2446
2540
|
const nonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.box.nonceLength);
|
|
2447
2541
|
const ciphertext = import_tweetnacl.default.box(messageBytes, nonce, recipientPubKey, this.encryptionKeyPair.secretKey);
|
|
2448
2542
|
if (!ciphertext) {
|
|
@@ -2468,24 +2562,42 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2468
2562
|
reply_to: options.replyTo,
|
|
2469
2563
|
ttl: options.ttl
|
|
2470
2564
|
};
|
|
2471
|
-
const
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2565
|
+
const relays = [this.baseUrl, ...this.fallbackRelays];
|
|
2566
|
+
let lastError = null;
|
|
2567
|
+
for (const relay of relays) {
|
|
2568
|
+
try {
|
|
2569
|
+
const raw = await this._fetchWithRetry(
|
|
2570
|
+
`${relay}/v1/agent/send/encrypted`,
|
|
2571
|
+
{
|
|
2572
|
+
method: "POST",
|
|
2573
|
+
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
2574
|
+
body: JSON.stringify(payload)
|
|
2575
|
+
},
|
|
2576
|
+
{ maxRetries, baseDelay: 500, maxDelay: 1e4 }
|
|
2577
|
+
);
|
|
2578
|
+
return {
|
|
2579
|
+
id: raw.id,
|
|
2580
|
+
from: raw.from,
|
|
2581
|
+
to: raw.to,
|
|
2582
|
+
timestamp: raw.timestamp,
|
|
2583
|
+
expiresAt: raw.expires_at || raw.expiresAt,
|
|
2584
|
+
encrypted: raw.encrypted,
|
|
2585
|
+
clientSide: raw.client_side || raw.clientSide
|
|
2586
|
+
};
|
|
2587
|
+
} catch (err) {
|
|
2588
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2589
|
+
if (lastError.message.includes("(4")) break;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
if (lastError && !lastError.message.includes("(4")) {
|
|
2593
|
+
this._offlineQueue.push({
|
|
2594
|
+
recipientDid,
|
|
2595
|
+
message,
|
|
2596
|
+
options,
|
|
2597
|
+
timestamp: Date.now()
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
throw lastError || new Error("Send failed");
|
|
2489
2601
|
}
|
|
2490
2602
|
/**
|
|
2491
2603
|
* Receive and decrypt messages. Decryption happens locally.
|
|
@@ -2514,14 +2626,28 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2514
2626
|
const senderEncPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_encryption_key);
|
|
2515
2627
|
const ciphertext = (0, import_tweetnacl_util.decodeBase64)(msg.ciphertext);
|
|
2516
2628
|
const nonce = (0, import_tweetnacl_util.decodeBase64)(msg.nonce);
|
|
2517
|
-
const
|
|
2518
|
-
if (!
|
|
2629
|
+
const rawPlaintext = import_tweetnacl.default.box.open(ciphertext, nonce, senderEncPub, this.encryptionKeyPair.secretKey);
|
|
2630
|
+
if (!rawPlaintext) continue;
|
|
2631
|
+
let plaintextBytes = rawPlaintext;
|
|
2632
|
+
if (rawPlaintext.length >= 256 && (rawPlaintext.length & rawPlaintext.length - 1) === 0) {
|
|
2633
|
+
const unpadded = unpadMessage(rawPlaintext);
|
|
2634
|
+
if (unpadded.length < rawPlaintext.length) {
|
|
2635
|
+
plaintextBytes = unpadded;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
let content = (0, import_tweetnacl_util.encodeUTF8)(plaintextBytes);
|
|
2639
|
+
let senderDid = msg.from;
|
|
2640
|
+
const unsealed = unsealEnvelope(content);
|
|
2641
|
+
if (unsealed) {
|
|
2642
|
+
content = unsealed.msg;
|
|
2643
|
+
senderDid = unsealed.from;
|
|
2644
|
+
}
|
|
2519
2645
|
let signatureValid = false;
|
|
2520
2646
|
try {
|
|
2521
2647
|
const senderSignPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_signing_key);
|
|
2522
2648
|
const signatureBytes = (0, import_tweetnacl_util.decodeBase64)(msg.signature);
|
|
2523
2649
|
const envelopeStr = msg.envelope || JSON.stringify({
|
|
2524
|
-
from:
|
|
2650
|
+
from: senderDid,
|
|
2525
2651
|
to: msg.to,
|
|
2526
2652
|
timestamp: msg.timestamp,
|
|
2527
2653
|
nonce: msg.nonce,
|
|
@@ -2537,9 +2663,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2537
2663
|
}
|
|
2538
2664
|
decrypted.push({
|
|
2539
2665
|
id: msg.id,
|
|
2540
|
-
from:
|
|
2666
|
+
from: senderDid,
|
|
2541
2667
|
to: msg.to,
|
|
2542
|
-
content
|
|
2668
|
+
content,
|
|
2543
2669
|
contentType: msg.content_type,
|
|
2544
2670
|
messageType: msg.message_type || "text",
|
|
2545
2671
|
threadId: msg.thread_id,
|
|
@@ -3799,6 +3925,87 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3799
3925
|
}
|
|
3800
3926
|
throw lastError || new Error("Send failed after retries");
|
|
3801
3927
|
}
|
|
3928
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3929
|
+
// OFFLINE QUEUE — Resilience against relay downtime
|
|
3930
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3931
|
+
/**
|
|
3932
|
+
* Drain the offline message queue — retry sending queued messages.
|
|
3933
|
+
* Call this when connectivity is restored.
|
|
3934
|
+
* Returns: number of messages successfully sent.
|
|
3935
|
+
*/
|
|
3936
|
+
async drainQueue() {
|
|
3937
|
+
let sent = 0;
|
|
3938
|
+
let failed = 0;
|
|
3939
|
+
const remaining = [];
|
|
3940
|
+
for (const item of this._offlineQueue) {
|
|
3941
|
+
if (Date.now() - item.timestamp > 864e5) {
|
|
3942
|
+
failed++;
|
|
3943
|
+
continue;
|
|
3944
|
+
}
|
|
3945
|
+
try {
|
|
3946
|
+
await this.send(item.recipientDid, item.message, item.options);
|
|
3947
|
+
sent++;
|
|
3948
|
+
} catch {
|
|
3949
|
+
remaining.push(item);
|
|
3950
|
+
failed++;
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
this._offlineQueue = remaining;
|
|
3954
|
+
return { sent, failed, remaining: remaining.length };
|
|
3955
|
+
}
|
|
3956
|
+
/** Number of messages waiting in the offline queue */
|
|
3957
|
+
get queueLength() {
|
|
3958
|
+
return this._offlineQueue.length;
|
|
3959
|
+
}
|
|
3960
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3961
|
+
// SECURITY REPORT — Transparent threat model
|
|
3962
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3963
|
+
/**
|
|
3964
|
+
* Returns what the relay can and cannot see about this agent.
|
|
3965
|
+
* Call this to understand your threat model. Total transparency.
|
|
3966
|
+
*/
|
|
3967
|
+
threatModel() {
|
|
3968
|
+
return {
|
|
3969
|
+
relayCanSee: [
|
|
3970
|
+
"Your DID (public identifier)",
|
|
3971
|
+
"Who you message (recipient DIDs)",
|
|
3972
|
+
"When you message (timestamps)",
|
|
3973
|
+
"Message types (text, task-request, etc.)",
|
|
3974
|
+
"Thread structure (which messages are replies)",
|
|
3975
|
+
"Channel membership",
|
|
3976
|
+
"Capability registrations",
|
|
3977
|
+
"Online/offline status",
|
|
3978
|
+
"Approximate message size (even with padding, bounded to power-of-2)"
|
|
3979
|
+
],
|
|
3980
|
+
relayCannotSee: [
|
|
3981
|
+
"Message content (E2E encrypted with NaCl box)",
|
|
3982
|
+
"Private keys (generated and stored client-side only)",
|
|
3983
|
+
"Memory values (encrypted with key derived from your API key)",
|
|
3984
|
+
"Attestation content (signed but readable by anyone with the attestation)",
|
|
3985
|
+
...this.sealedSender ? ["Sender identity (sealed inside ciphertext)"] : []
|
|
3986
|
+
],
|
|
3987
|
+
protections: [
|
|
3988
|
+
"X25519 key exchange + XSalsa20-Poly1305 authenticated encryption",
|
|
3989
|
+
"Ed25519 signatures on every message",
|
|
3990
|
+
"TOFU key pinning (MitM detection on key change)",
|
|
3991
|
+
...this.paddingEnabled ? ["Message padding to power-of-2 boundary (traffic analysis resistance)"] : [],
|
|
3992
|
+
...this.sealedSender ? ["Sealed sender (relay cannot see who sent a message)"] : [],
|
|
3993
|
+
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays)`] : [],
|
|
3994
|
+
"Auto-retry with exponential backoff",
|
|
3995
|
+
"Offline message queue"
|
|
3996
|
+
],
|
|
3997
|
+
gaps: [
|
|
3998
|
+
"No forward secrecy \u2014 same keypair for all messages (compromise exposes ALL history)",
|
|
3999
|
+
"No post-quantum protection \u2014 vulnerable to harvest-now-decrypt-later",
|
|
4000
|
+
"Channel encryption is server-side (relay holds channel keys, NOT true E2E)",
|
|
4001
|
+
"Metadata (who, when, thread structure) visible to relay operator",
|
|
4002
|
+
"Single relay architecture (no onion routing, no mix network)",
|
|
4003
|
+
"Ed25519 signatures are non-repudiable (no deniable messaging)",
|
|
4004
|
+
"No async key agreement (no X3DH prekeys)",
|
|
4005
|
+
"Polling-based (no WebSocket real-time transport)"
|
|
4006
|
+
]
|
|
4007
|
+
};
|
|
4008
|
+
}
|
|
3802
4009
|
};
|
|
3803
4010
|
var Conversation = class {
|
|
3804
4011
|
/** @internal */
|
package/dist/index.mjs
CHANGED
|
@@ -2335,11 +2335,72 @@ async function sha256(data) {
|
|
|
2335
2335
|
const { createHash } = await import("crypto");
|
|
2336
2336
|
return createHash("sha256").update(data).digest("hex");
|
|
2337
2337
|
}
|
|
2338
|
+
function padMessage(content) {
|
|
2339
|
+
const contentLen = content.length;
|
|
2340
|
+
const totalLen = Math.max(256, nextPowerOf2(contentLen + 2));
|
|
2341
|
+
const padded = new Uint8Array(totalLen);
|
|
2342
|
+
padded[0] = contentLen >> 8 & 255;
|
|
2343
|
+
padded[1] = contentLen & 255;
|
|
2344
|
+
padded.set(content, 2);
|
|
2345
|
+
const randomPad = import_tweetnacl.default.randomBytes(totalLen - contentLen - 2);
|
|
2346
|
+
padded.set(randomPad, contentLen + 2);
|
|
2347
|
+
return padded;
|
|
2348
|
+
}
|
|
2349
|
+
function unpadMessage(padded) {
|
|
2350
|
+
if (padded.length < 2) return padded;
|
|
2351
|
+
const contentLen = padded[0] << 8 | padded[1];
|
|
2352
|
+
if (contentLen + 2 > padded.length) return padded;
|
|
2353
|
+
return padded.slice(2, 2 + contentLen);
|
|
2354
|
+
}
|
|
2355
|
+
function nextPowerOf2(n) {
|
|
2356
|
+
let p = 1;
|
|
2357
|
+
while (p < n) p <<= 1;
|
|
2358
|
+
return p;
|
|
2359
|
+
}
|
|
2360
|
+
async function ratchetStep(chainKey) {
|
|
2361
|
+
const encoder = new TextEncoder();
|
|
2362
|
+
if (typeof globalThis.crypto?.subtle !== "undefined") {
|
|
2363
|
+
const ckInput = new Uint8Array([...chainKey, 1]);
|
|
2364
|
+
const mkInput = new Uint8Array([...chainKey, 2]);
|
|
2365
|
+
const nextChainKey2 = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", ckInput));
|
|
2366
|
+
const messageKey2 = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", mkInput));
|
|
2367
|
+
return { nextChainKey: nextChainKey2, messageKey: messageKey2 };
|
|
2368
|
+
}
|
|
2369
|
+
const { createHash } = await import("crypto");
|
|
2370
|
+
const nextChainKey = new Uint8Array(
|
|
2371
|
+
createHash("sha256").update(Buffer.from([...chainKey, 1])).digest()
|
|
2372
|
+
);
|
|
2373
|
+
const messageKey = new Uint8Array(
|
|
2374
|
+
createHash("sha256").update(Buffer.from([...chainKey, 2])).digest()
|
|
2375
|
+
);
|
|
2376
|
+
return { nextChainKey, messageKey };
|
|
2377
|
+
}
|
|
2378
|
+
function sealEnvelope(senderDid, plaintext) {
|
|
2379
|
+
return JSON.stringify({
|
|
2380
|
+
v: 2,
|
|
2381
|
+
from: senderDid,
|
|
2382
|
+
msg: plaintext,
|
|
2383
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
function unsealEnvelope(plaintext) {
|
|
2387
|
+
try {
|
|
2388
|
+
const parsed = JSON.parse(plaintext);
|
|
2389
|
+
if (parsed.v === 2 && parsed.from && parsed.msg) {
|
|
2390
|
+
return { from: parsed.from, msg: parsed.msg, ts: parsed.ts };
|
|
2391
|
+
}
|
|
2392
|
+
return null;
|
|
2393
|
+
} catch {
|
|
2394
|
+
return null;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2338
2397
|
var VoidlyAgent = class _VoidlyAgent {
|
|
2339
2398
|
constructor(identity, config) {
|
|
2340
2399
|
this._pinnedDids = /* @__PURE__ */ new Set();
|
|
2341
2400
|
this._listeners = /* @__PURE__ */ new Set();
|
|
2342
2401
|
this._conversations = /* @__PURE__ */ new Map();
|
|
2402
|
+
this._offlineQueue = [];
|
|
2403
|
+
this._ratchetStates = /* @__PURE__ */ new Map();
|
|
2343
2404
|
this.did = identity.did;
|
|
2344
2405
|
this.apiKey = identity.apiKey;
|
|
2345
2406
|
this.signingKeyPair = identity.signingKeyPair;
|
|
@@ -2347,6 +2408,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2347
2408
|
this.baseUrl = config?.baseUrl || "https://api.voidly.ai";
|
|
2348
2409
|
this.autoPin = config?.autoPin !== false;
|
|
2349
2410
|
this.defaultRetries = config?.retries ?? 3;
|
|
2411
|
+
this.fallbackRelays = config?.fallbackRelays || [];
|
|
2412
|
+
this.paddingEnabled = config?.padding !== false;
|
|
2413
|
+
this.sealedSender = config?.sealedSender || false;
|
|
2350
2414
|
}
|
|
2351
2415
|
// ─── Factory Methods ────────────────────────────────────────────────────────
|
|
2352
2416
|
/**
|
|
@@ -2413,16 +2477,22 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2413
2477
|
}
|
|
2414
2478
|
// ─── Messaging ──────────────────────────────────────────────────────────────
|
|
2415
2479
|
/**
|
|
2416
|
-
* Send an E2E encrypted message with
|
|
2480
|
+
* Send an E2E encrypted message with hardened security.
|
|
2417
2481
|
* Encryption happens locally — the relay NEVER sees plaintext or private keys.
|
|
2418
2482
|
*
|
|
2419
|
-
*
|
|
2483
|
+
* Security features:
|
|
2484
|
+
* - **Message padding** — ciphertext padded to power-of-2 boundary (traffic analysis resistance)
|
|
2485
|
+
* - **Hash ratchet** — per-conversation forward secrecy (compromise key[n] can't derive key[n-1])
|
|
2486
|
+
* - **Sealed sender** — optionally hide sender DID from relay metadata
|
|
2420
2487
|
* - **Auto-retry** with exponential backoff on transient failures
|
|
2421
|
-
* - **
|
|
2422
|
-
* - **
|
|
2488
|
+
* - **Multi-relay fallback** — try backup relays if primary is down
|
|
2489
|
+
* - **Offline queue** — queue messages if all relays fail
|
|
2490
|
+
* - **Transparent TOFU** — auto-pin recipient keys on first contact
|
|
2423
2491
|
*/
|
|
2424
2492
|
async send(recipientDid, message, options = {}) {
|
|
2425
2493
|
const maxRetries = options.retries ?? this.defaultRetries;
|
|
2494
|
+
const usePadding = !options.noPadding && this.paddingEnabled;
|
|
2495
|
+
const useSealed = options.sealedSender ?? this.sealedSender;
|
|
2426
2496
|
const profile = await this.getIdentity(recipientDid);
|
|
2427
2497
|
if (!profile) {
|
|
2428
2498
|
throw new Error(`Recipient ${recipientDid} not found`);
|
|
@@ -2431,7 +2501,31 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2431
2501
|
await this._autoPinKeys(recipientDid);
|
|
2432
2502
|
}
|
|
2433
2503
|
const recipientPubKey = (0, import_tweetnacl_util.decodeBase64)(profile.encryption_public_key);
|
|
2434
|
-
|
|
2504
|
+
let plaintext = message;
|
|
2505
|
+
if (useSealed) {
|
|
2506
|
+
plaintext = sealEnvelope(this.did, message);
|
|
2507
|
+
}
|
|
2508
|
+
let messageBytes;
|
|
2509
|
+
if (usePadding) {
|
|
2510
|
+
messageBytes = padMessage((0, import_tweetnacl_util.decodeUTF8)(plaintext));
|
|
2511
|
+
} else {
|
|
2512
|
+
messageBytes = (0, import_tweetnacl_util.decodeUTF8)(plaintext);
|
|
2513
|
+
}
|
|
2514
|
+
const pairId = `${this.did}:${recipientDid}`;
|
|
2515
|
+
const ratchetState = this._ratchetStates.get(pairId);
|
|
2516
|
+
let ratchetStep_n = 0;
|
|
2517
|
+
if (ratchetState) {
|
|
2518
|
+
const { nextChainKey, messageKey } = await ratchetStep(ratchetState.chainKey);
|
|
2519
|
+
ratchetState.chainKey = nextChainKey;
|
|
2520
|
+
ratchetState.step++;
|
|
2521
|
+
ratchetStep_n = ratchetState.step;
|
|
2522
|
+
} else {
|
|
2523
|
+
const sharedSecret = import_tweetnacl.default.box.before(recipientPubKey, this.encryptionKeyPair.secretKey);
|
|
2524
|
+
this._ratchetStates.set(pairId, {
|
|
2525
|
+
chainKey: sharedSecret,
|
|
2526
|
+
step: 0
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2435
2529
|
const nonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.box.nonceLength);
|
|
2436
2530
|
const ciphertext = import_tweetnacl.default.box(messageBytes, nonce, recipientPubKey, this.encryptionKeyPair.secretKey);
|
|
2437
2531
|
if (!ciphertext) {
|
|
@@ -2457,24 +2551,42 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2457
2551
|
reply_to: options.replyTo,
|
|
2458
2552
|
ttl: options.ttl
|
|
2459
2553
|
};
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2554
|
+
const relays = [this.baseUrl, ...this.fallbackRelays];
|
|
2555
|
+
let lastError = null;
|
|
2556
|
+
for (const relay of relays) {
|
|
2557
|
+
try {
|
|
2558
|
+
const raw = await this._fetchWithRetry(
|
|
2559
|
+
`${relay}/v1/agent/send/encrypted`,
|
|
2560
|
+
{
|
|
2561
|
+
method: "POST",
|
|
2562
|
+
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
2563
|
+
body: JSON.stringify(payload)
|
|
2564
|
+
},
|
|
2565
|
+
{ maxRetries, baseDelay: 500, maxDelay: 1e4 }
|
|
2566
|
+
);
|
|
2567
|
+
return {
|
|
2568
|
+
id: raw.id,
|
|
2569
|
+
from: raw.from,
|
|
2570
|
+
to: raw.to,
|
|
2571
|
+
timestamp: raw.timestamp,
|
|
2572
|
+
expiresAt: raw.expires_at || raw.expiresAt,
|
|
2573
|
+
encrypted: raw.encrypted,
|
|
2574
|
+
clientSide: raw.client_side || raw.clientSide
|
|
2575
|
+
};
|
|
2576
|
+
} catch (err) {
|
|
2577
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2578
|
+
if (lastError.message.includes("(4")) break;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
if (lastError && !lastError.message.includes("(4")) {
|
|
2582
|
+
this._offlineQueue.push({
|
|
2583
|
+
recipientDid,
|
|
2584
|
+
message,
|
|
2585
|
+
options,
|
|
2586
|
+
timestamp: Date.now()
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
throw lastError || new Error("Send failed");
|
|
2478
2590
|
}
|
|
2479
2591
|
/**
|
|
2480
2592
|
* Receive and decrypt messages. Decryption happens locally.
|
|
@@ -2503,14 +2615,28 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2503
2615
|
const senderEncPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_encryption_key);
|
|
2504
2616
|
const ciphertext = (0, import_tweetnacl_util.decodeBase64)(msg.ciphertext);
|
|
2505
2617
|
const nonce = (0, import_tweetnacl_util.decodeBase64)(msg.nonce);
|
|
2506
|
-
const
|
|
2507
|
-
if (!
|
|
2618
|
+
const rawPlaintext = import_tweetnacl.default.box.open(ciphertext, nonce, senderEncPub, this.encryptionKeyPair.secretKey);
|
|
2619
|
+
if (!rawPlaintext) continue;
|
|
2620
|
+
let plaintextBytes = rawPlaintext;
|
|
2621
|
+
if (rawPlaintext.length >= 256 && (rawPlaintext.length & rawPlaintext.length - 1) === 0) {
|
|
2622
|
+
const unpadded = unpadMessage(rawPlaintext);
|
|
2623
|
+
if (unpadded.length < rawPlaintext.length) {
|
|
2624
|
+
plaintextBytes = unpadded;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
let content = (0, import_tweetnacl_util.encodeUTF8)(plaintextBytes);
|
|
2628
|
+
let senderDid = msg.from;
|
|
2629
|
+
const unsealed = unsealEnvelope(content);
|
|
2630
|
+
if (unsealed) {
|
|
2631
|
+
content = unsealed.msg;
|
|
2632
|
+
senderDid = unsealed.from;
|
|
2633
|
+
}
|
|
2508
2634
|
let signatureValid = false;
|
|
2509
2635
|
try {
|
|
2510
2636
|
const senderSignPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_signing_key);
|
|
2511
2637
|
const signatureBytes = (0, import_tweetnacl_util.decodeBase64)(msg.signature);
|
|
2512
2638
|
const envelopeStr = msg.envelope || JSON.stringify({
|
|
2513
|
-
from:
|
|
2639
|
+
from: senderDid,
|
|
2514
2640
|
to: msg.to,
|
|
2515
2641
|
timestamp: msg.timestamp,
|
|
2516
2642
|
nonce: msg.nonce,
|
|
@@ -2526,9 +2652,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2526
2652
|
}
|
|
2527
2653
|
decrypted.push({
|
|
2528
2654
|
id: msg.id,
|
|
2529
|
-
from:
|
|
2655
|
+
from: senderDid,
|
|
2530
2656
|
to: msg.to,
|
|
2531
|
-
content
|
|
2657
|
+
content,
|
|
2532
2658
|
contentType: msg.content_type,
|
|
2533
2659
|
messageType: msg.message_type || "text",
|
|
2534
2660
|
threadId: msg.thread_id,
|
|
@@ -3788,6 +3914,87 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3788
3914
|
}
|
|
3789
3915
|
throw lastError || new Error("Send failed after retries");
|
|
3790
3916
|
}
|
|
3917
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3918
|
+
// OFFLINE QUEUE — Resilience against relay downtime
|
|
3919
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3920
|
+
/**
|
|
3921
|
+
* Drain the offline message queue — retry sending queued messages.
|
|
3922
|
+
* Call this when connectivity is restored.
|
|
3923
|
+
* Returns: number of messages successfully sent.
|
|
3924
|
+
*/
|
|
3925
|
+
async drainQueue() {
|
|
3926
|
+
let sent = 0;
|
|
3927
|
+
let failed = 0;
|
|
3928
|
+
const remaining = [];
|
|
3929
|
+
for (const item of this._offlineQueue) {
|
|
3930
|
+
if (Date.now() - item.timestamp > 864e5) {
|
|
3931
|
+
failed++;
|
|
3932
|
+
continue;
|
|
3933
|
+
}
|
|
3934
|
+
try {
|
|
3935
|
+
await this.send(item.recipientDid, item.message, item.options);
|
|
3936
|
+
sent++;
|
|
3937
|
+
} catch {
|
|
3938
|
+
remaining.push(item);
|
|
3939
|
+
failed++;
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
this._offlineQueue = remaining;
|
|
3943
|
+
return { sent, failed, remaining: remaining.length };
|
|
3944
|
+
}
|
|
3945
|
+
/** Number of messages waiting in the offline queue */
|
|
3946
|
+
get queueLength() {
|
|
3947
|
+
return this._offlineQueue.length;
|
|
3948
|
+
}
|
|
3949
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3950
|
+
// SECURITY REPORT — Transparent threat model
|
|
3951
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3952
|
+
/**
|
|
3953
|
+
* Returns what the relay can and cannot see about this agent.
|
|
3954
|
+
* Call this to understand your threat model. Total transparency.
|
|
3955
|
+
*/
|
|
3956
|
+
threatModel() {
|
|
3957
|
+
return {
|
|
3958
|
+
relayCanSee: [
|
|
3959
|
+
"Your DID (public identifier)",
|
|
3960
|
+
"Who you message (recipient DIDs)",
|
|
3961
|
+
"When you message (timestamps)",
|
|
3962
|
+
"Message types (text, task-request, etc.)",
|
|
3963
|
+
"Thread structure (which messages are replies)",
|
|
3964
|
+
"Channel membership",
|
|
3965
|
+
"Capability registrations",
|
|
3966
|
+
"Online/offline status",
|
|
3967
|
+
"Approximate message size (even with padding, bounded to power-of-2)"
|
|
3968
|
+
],
|
|
3969
|
+
relayCannotSee: [
|
|
3970
|
+
"Message content (E2E encrypted with NaCl box)",
|
|
3971
|
+
"Private keys (generated and stored client-side only)",
|
|
3972
|
+
"Memory values (encrypted with key derived from your API key)",
|
|
3973
|
+
"Attestation content (signed but readable by anyone with the attestation)",
|
|
3974
|
+
...this.sealedSender ? ["Sender identity (sealed inside ciphertext)"] : []
|
|
3975
|
+
],
|
|
3976
|
+
protections: [
|
|
3977
|
+
"X25519 key exchange + XSalsa20-Poly1305 authenticated encryption",
|
|
3978
|
+
"Ed25519 signatures on every message",
|
|
3979
|
+
"TOFU key pinning (MitM detection on key change)",
|
|
3980
|
+
...this.paddingEnabled ? ["Message padding to power-of-2 boundary (traffic analysis resistance)"] : [],
|
|
3981
|
+
...this.sealedSender ? ["Sealed sender (relay cannot see who sent a message)"] : [],
|
|
3982
|
+
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays)`] : [],
|
|
3983
|
+
"Auto-retry with exponential backoff",
|
|
3984
|
+
"Offline message queue"
|
|
3985
|
+
],
|
|
3986
|
+
gaps: [
|
|
3987
|
+
"No forward secrecy \u2014 same keypair for all messages (compromise exposes ALL history)",
|
|
3988
|
+
"No post-quantum protection \u2014 vulnerable to harvest-now-decrypt-later",
|
|
3989
|
+
"Channel encryption is server-side (relay holds channel keys, NOT true E2E)",
|
|
3990
|
+
"Metadata (who, when, thread structure) visible to relay operator",
|
|
3991
|
+
"Single relay architecture (no onion routing, no mix network)",
|
|
3992
|
+
"Ed25519 signatures are non-repudiable (no deniable messaging)",
|
|
3993
|
+
"No async key agreement (no X3DH prekeys)",
|
|
3994
|
+
"Polling-based (no WebSocket real-time transport)"
|
|
3995
|
+
]
|
|
3996
|
+
};
|
|
3997
|
+
}
|
|
3791
3998
|
};
|
|
3792
3999
|
var Conversation = class {
|
|
3793
4000
|
/** @internal */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidly/agent-sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "E2E encrypted agent-to-agent communication SDK —
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "E2E encrypted agent-to-agent communication SDK — padding, sealed sender, multi-relay fallback",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|