@voidly/agent-sdk 2.0.0 → 2.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 +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +311 -55
- package/dist/index.mjs +311 -55
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -60,6 +60,10 @@ interface VoidlyAgentConfig {
|
|
|
60
60
|
padding?: boolean;
|
|
61
61
|
/** Enable sealed sender — hide sender DID from relay metadata (default: false) */
|
|
62
62
|
sealedSender?: boolean;
|
|
63
|
+
/** Reject messages with invalid signatures (default: false — returns signatureValid: false) */
|
|
64
|
+
requireSignatures?: boolean;
|
|
65
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
66
|
+
timeout?: number;
|
|
63
67
|
}
|
|
64
68
|
interface ListenOptions {
|
|
65
69
|
/** Milliseconds between polls (default: 2000, min: 500) */
|
|
@@ -137,11 +141,16 @@ declare class VoidlyAgent {
|
|
|
137
141
|
private fallbackRelays;
|
|
138
142
|
private paddingEnabled;
|
|
139
143
|
private sealedSender;
|
|
144
|
+
private requireSignatures;
|
|
145
|
+
private timeout;
|
|
140
146
|
private _pinnedDids;
|
|
141
147
|
private _listeners;
|
|
142
148
|
private _conversations;
|
|
143
149
|
private _offlineQueue;
|
|
144
150
|
private _ratchetStates;
|
|
151
|
+
private _identityCache;
|
|
152
|
+
private _seenMessageIds;
|
|
153
|
+
private _decryptFailCount;
|
|
145
154
|
private constructor();
|
|
146
155
|
/**
|
|
147
156
|
* Register a new agent on the Voidly relay.
|
|
@@ -173,6 +182,17 @@ declare class VoidlyAgent {
|
|
|
173
182
|
signingPublicKey: string;
|
|
174
183
|
encryptionPublicKey: string;
|
|
175
184
|
};
|
|
185
|
+
/**
|
|
186
|
+
* Get the number of messages that failed to decrypt.
|
|
187
|
+
* Useful for detecting key mismatches, attacks, or corruption.
|
|
188
|
+
*/
|
|
189
|
+
get decryptFailCount(): number;
|
|
190
|
+
/**
|
|
191
|
+
* Generate a did:key identifier from this agent's Ed25519 signing key.
|
|
192
|
+
* did:key is a W3C standard — interoperable across systems.
|
|
193
|
+
* Format: did:key:z6Mk{base58-multicodec-ed25519-pubkey}
|
|
194
|
+
*/
|
|
195
|
+
get didKey(): string;
|
|
176
196
|
/**
|
|
177
197
|
* Send an E2E encrypted message with hardened security.
|
|
178
198
|
* Encryption happens locally — the relay NEVER sees plaintext or private keys.
|
|
@@ -644,7 +664,8 @@ declare class VoidlyAgent {
|
|
|
644
664
|
}, signingPublicKey: string): boolean;
|
|
645
665
|
/**
|
|
646
666
|
* Store an encrypted key-value pair in persistent memory.
|
|
647
|
-
* Values are encrypted with
|
|
667
|
+
* Values are encrypted CLIENT-SIDE with nacl.secretbox before sending to relay.
|
|
668
|
+
* The relay never sees plaintext values — true E2E encrypted storage.
|
|
648
669
|
*/
|
|
649
670
|
memorySet(namespace: string, key: string, value: unknown, options?: {
|
|
650
671
|
valueType?: string;
|
|
@@ -657,7 +678,7 @@ declare class VoidlyAgent {
|
|
|
657
678
|
}>;
|
|
658
679
|
/**
|
|
659
680
|
* Retrieve a value from persistent memory.
|
|
660
|
-
* Decrypted
|
|
681
|
+
* Decrypted CLIENT-SIDE — relay never sees plaintext.
|
|
661
682
|
*/
|
|
662
683
|
memoryGet(namespace: string, key: string): Promise<{
|
|
663
684
|
namespace: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -60,6 +60,10 @@ interface VoidlyAgentConfig {
|
|
|
60
60
|
padding?: boolean;
|
|
61
61
|
/** Enable sealed sender — hide sender DID from relay metadata (default: false) */
|
|
62
62
|
sealedSender?: boolean;
|
|
63
|
+
/** Reject messages with invalid signatures (default: false — returns signatureValid: false) */
|
|
64
|
+
requireSignatures?: boolean;
|
|
65
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
66
|
+
timeout?: number;
|
|
63
67
|
}
|
|
64
68
|
interface ListenOptions {
|
|
65
69
|
/** Milliseconds between polls (default: 2000, min: 500) */
|
|
@@ -137,11 +141,16 @@ declare class VoidlyAgent {
|
|
|
137
141
|
private fallbackRelays;
|
|
138
142
|
private paddingEnabled;
|
|
139
143
|
private sealedSender;
|
|
144
|
+
private requireSignatures;
|
|
145
|
+
private timeout;
|
|
140
146
|
private _pinnedDids;
|
|
141
147
|
private _listeners;
|
|
142
148
|
private _conversations;
|
|
143
149
|
private _offlineQueue;
|
|
144
150
|
private _ratchetStates;
|
|
151
|
+
private _identityCache;
|
|
152
|
+
private _seenMessageIds;
|
|
153
|
+
private _decryptFailCount;
|
|
145
154
|
private constructor();
|
|
146
155
|
/**
|
|
147
156
|
* Register a new agent on the Voidly relay.
|
|
@@ -173,6 +182,17 @@ declare class VoidlyAgent {
|
|
|
173
182
|
signingPublicKey: string;
|
|
174
183
|
encryptionPublicKey: string;
|
|
175
184
|
};
|
|
185
|
+
/**
|
|
186
|
+
* Get the number of messages that failed to decrypt.
|
|
187
|
+
* Useful for detecting key mismatches, attacks, or corruption.
|
|
188
|
+
*/
|
|
189
|
+
get decryptFailCount(): number;
|
|
190
|
+
/**
|
|
191
|
+
* Generate a did:key identifier from this agent's Ed25519 signing key.
|
|
192
|
+
* did:key is a W3C standard — interoperable across systems.
|
|
193
|
+
* Format: did:key:z6Mk{base58-multicodec-ed25519-pubkey}
|
|
194
|
+
*/
|
|
195
|
+
get didKey(): string;
|
|
176
196
|
/**
|
|
177
197
|
* Send an E2E encrypted message with hardened security.
|
|
178
198
|
* Encryption happens locally — the relay NEVER sees plaintext or private keys.
|
|
@@ -644,7 +664,8 @@ declare class VoidlyAgent {
|
|
|
644
664
|
}, signingPublicKey: string): boolean;
|
|
645
665
|
/**
|
|
646
666
|
* Store an encrypted key-value pair in persistent memory.
|
|
647
|
-
* Values are encrypted with
|
|
667
|
+
* Values are encrypted CLIENT-SIDE with nacl.secretbox before sending to relay.
|
|
668
|
+
* The relay never sees plaintext values — true E2E encrypted storage.
|
|
648
669
|
*/
|
|
649
670
|
memorySet(namespace: string, key: string, value: unknown, options?: {
|
|
650
671
|
valueType?: string;
|
|
@@ -657,7 +678,7 @@ declare class VoidlyAgent {
|
|
|
657
678
|
}>;
|
|
658
679
|
/**
|
|
659
680
|
* Retrieve a value from persistent memory.
|
|
660
|
-
* Decrypted
|
|
681
|
+
* Decrypted CLIENT-SIDE — relay never sees plaintext.
|
|
661
682
|
*/
|
|
662
683
|
memoryGet(namespace: string, key: string): Promise<{
|
|
663
684
|
namespace: string;
|
package/dist/index.js
CHANGED
|
@@ -2405,6 +2405,38 @@ function unsealEnvelope(plaintext) {
|
|
|
2405
2405
|
return null;
|
|
2406
2406
|
}
|
|
2407
2407
|
}
|
|
2408
|
+
var PROTO_MARKER = 86;
|
|
2409
|
+
var FLAG_PADDED = 1;
|
|
2410
|
+
var FLAG_SEALED = 2;
|
|
2411
|
+
var FLAG_RATCHET = 4;
|
|
2412
|
+
function makeProtoHeader(flags, ratchetStep2) {
|
|
2413
|
+
return new Uint8Array([PROTO_MARKER, flags, ratchetStep2 >> 8 & 255, ratchetStep2 & 255]);
|
|
2414
|
+
}
|
|
2415
|
+
function parseProtoHeader(data) {
|
|
2416
|
+
if (data.length < 4 || data[0] !== PROTO_MARKER) return null;
|
|
2417
|
+
return {
|
|
2418
|
+
flags: data[1],
|
|
2419
|
+
ratchetStep: data[2] << 8 | data[3],
|
|
2420
|
+
content: data.slice(4)
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
var MAX_SKIP = 200;
|
|
2424
|
+
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
2425
|
+
function toBase58(bytes) {
|
|
2426
|
+
if (bytes.length === 0) return "1";
|
|
2427
|
+
let result = "";
|
|
2428
|
+
let num = BigInt("0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""));
|
|
2429
|
+
while (num > 0n) {
|
|
2430
|
+
const remainder = num % 58n;
|
|
2431
|
+
num = num / 58n;
|
|
2432
|
+
result = BASE58_ALPHABET[Number(remainder)] + result;
|
|
2433
|
+
}
|
|
2434
|
+
for (const byte of bytes) {
|
|
2435
|
+
if (byte === 0) result = "1" + result;
|
|
2436
|
+
else break;
|
|
2437
|
+
}
|
|
2438
|
+
return result || "1";
|
|
2439
|
+
}
|
|
2408
2440
|
var VoidlyAgent = class _VoidlyAgent {
|
|
2409
2441
|
constructor(identity, config) {
|
|
2410
2442
|
this._pinnedDids = /* @__PURE__ */ new Set();
|
|
@@ -2412,6 +2444,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2412
2444
|
this._conversations = /* @__PURE__ */ new Map();
|
|
2413
2445
|
this._offlineQueue = [];
|
|
2414
2446
|
this._ratchetStates = /* @__PURE__ */ new Map();
|
|
2447
|
+
this._identityCache = /* @__PURE__ */ new Map();
|
|
2448
|
+
this._seenMessageIds = /* @__PURE__ */ new Set();
|
|
2449
|
+
this._decryptFailCount = 0;
|
|
2415
2450
|
this.did = identity.did;
|
|
2416
2451
|
this.apiKey = identity.apiKey;
|
|
2417
2452
|
this.signingKeyPair = identity.signingKeyPair;
|
|
@@ -2422,6 +2457,8 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2422
2457
|
this.fallbackRelays = config?.fallbackRelays || [];
|
|
2423
2458
|
this.paddingEnabled = config?.padding !== false;
|
|
2424
2459
|
this.sealedSender = config?.sealedSender || false;
|
|
2460
|
+
this.requireSignatures = config?.requireSignatures || false;
|
|
2461
|
+
this.timeout = config?.timeout ?? 3e4;
|
|
2425
2462
|
}
|
|
2426
2463
|
// ─── Factory Methods ────────────────────────────────────────────────────────
|
|
2427
2464
|
/**
|
|
@@ -2460,8 +2497,29 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2460
2497
|
* Use this to resume an agent across sessions.
|
|
2461
2498
|
*/
|
|
2462
2499
|
static fromCredentials(creds, config) {
|
|
2463
|
-
|
|
2464
|
-
|
|
2500
|
+
if (!creds.did || !creds.did.startsWith("did:")) {
|
|
2501
|
+
throw new Error('Invalid credentials: did must start with "did:"');
|
|
2502
|
+
}
|
|
2503
|
+
if (!creds.apiKey || creds.apiKey.length < 8) {
|
|
2504
|
+
throw new Error("Invalid credentials: apiKey is missing or too short");
|
|
2505
|
+
}
|
|
2506
|
+
if (!creds.signingSecretKey || !creds.encryptionSecretKey) {
|
|
2507
|
+
throw new Error("Invalid credentials: secret keys are required");
|
|
2508
|
+
}
|
|
2509
|
+
let signingSecret;
|
|
2510
|
+
let encryptionSecret;
|
|
2511
|
+
try {
|
|
2512
|
+
signingSecret = (0, import_tweetnacl_util.decodeBase64)(creds.signingSecretKey);
|
|
2513
|
+
encryptionSecret = (0, import_tweetnacl_util.decodeBase64)(creds.encryptionSecretKey);
|
|
2514
|
+
} catch {
|
|
2515
|
+
throw new Error("Invalid credentials: secret keys must be valid base64");
|
|
2516
|
+
}
|
|
2517
|
+
if (signingSecret.length !== 64) {
|
|
2518
|
+
throw new Error(`Invalid credentials: signing key must be 64 bytes, got ${signingSecret.length}`);
|
|
2519
|
+
}
|
|
2520
|
+
if (encryptionSecret.length !== 32) {
|
|
2521
|
+
throw new Error(`Invalid credentials: encryption key must be 32 bytes, got ${encryptionSecret.length}`);
|
|
2522
|
+
}
|
|
2465
2523
|
return new _VoidlyAgent({
|
|
2466
2524
|
did: creds.did,
|
|
2467
2525
|
apiKey: creds.apiKey,
|
|
@@ -2486,6 +2544,25 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2486
2544
|
encryptionPublicKey: (0, import_tweetnacl_util.encodeBase64)(this.encryptionKeyPair.publicKey)
|
|
2487
2545
|
};
|
|
2488
2546
|
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Get the number of messages that failed to decrypt.
|
|
2549
|
+
* Useful for detecting key mismatches, attacks, or corruption.
|
|
2550
|
+
*/
|
|
2551
|
+
get decryptFailCount() {
|
|
2552
|
+
return this._decryptFailCount;
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Generate a did:key identifier from this agent's Ed25519 signing key.
|
|
2556
|
+
* did:key is a W3C standard — interoperable across systems.
|
|
2557
|
+
* Format: did:key:z6Mk{base58-multicodec-ed25519-pubkey}
|
|
2558
|
+
*/
|
|
2559
|
+
get didKey() {
|
|
2560
|
+
const multicodec = new Uint8Array(2 + this.signingKeyPair.publicKey.length);
|
|
2561
|
+
multicodec[0] = 237;
|
|
2562
|
+
multicodec[1] = 1;
|
|
2563
|
+
multicodec.set(this.signingKeyPair.publicKey, 2);
|
|
2564
|
+
return `did:key:z${toBase58(multicodec)}`;
|
|
2565
|
+
}
|
|
2489
2566
|
// ─── Messaging ──────────────────────────────────────────────────────────────
|
|
2490
2567
|
/**
|
|
2491
2568
|
* Send an E2E encrypted message with hardened security.
|
|
@@ -2516,29 +2593,39 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2516
2593
|
if (useSealed) {
|
|
2517
2594
|
plaintext = sealEnvelope(this.did, message);
|
|
2518
2595
|
}
|
|
2519
|
-
let
|
|
2596
|
+
let contentBytes;
|
|
2520
2597
|
if (usePadding) {
|
|
2521
|
-
|
|
2598
|
+
contentBytes = padMessage((0, import_tweetnacl_util.decodeUTF8)(plaintext));
|
|
2522
2599
|
} else {
|
|
2523
|
-
|
|
2600
|
+
contentBytes = (0, import_tweetnacl_util.decodeUTF8)(plaintext);
|
|
2524
2601
|
}
|
|
2525
2602
|
const pairId = `${this.did}:${recipientDid}`;
|
|
2526
|
-
|
|
2527
|
-
|
|
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 {
|
|
2603
|
+
let state = this._ratchetStates.get(pairId);
|
|
2604
|
+
if (!state) {
|
|
2534
2605
|
const sharedSecret = import_tweetnacl.default.box.before(recipientPubKey, this.encryptionKeyPair.secretKey);
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2606
|
+
state = {
|
|
2607
|
+
sendChainKey: sharedSecret,
|
|
2608
|
+
sendStep: 0,
|
|
2609
|
+
recvChainKey: sharedSecret,
|
|
2610
|
+
// Will be synced on first receive
|
|
2611
|
+
recvStep: 0,
|
|
2612
|
+
skippedKeys: /* @__PURE__ */ new Map()
|
|
2613
|
+
};
|
|
2614
|
+
this._ratchetStates.set(pairId, state);
|
|
2539
2615
|
}
|
|
2540
|
-
const
|
|
2541
|
-
|
|
2616
|
+
const { nextChainKey, messageKey } = await ratchetStep(state.sendChainKey);
|
|
2617
|
+
state.sendChainKey = nextChainKey;
|
|
2618
|
+
state.sendStep++;
|
|
2619
|
+
const currentStep = state.sendStep;
|
|
2620
|
+
let flags = FLAG_RATCHET;
|
|
2621
|
+
if (usePadding) flags |= FLAG_PADDED;
|
|
2622
|
+
if (useSealed) flags |= FLAG_SEALED;
|
|
2623
|
+
const header = makeProtoHeader(flags, currentStep);
|
|
2624
|
+
const messageBytes = new Uint8Array(header.length + contentBytes.length);
|
|
2625
|
+
messageBytes.set(header, 0);
|
|
2626
|
+
messageBytes.set(contentBytes, header.length);
|
|
2627
|
+
const nonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.secretbox.nonceLength);
|
|
2628
|
+
const ciphertext = import_tweetnacl.default.secretbox(messageBytes, nonce, messageKey);
|
|
2542
2629
|
if (!ciphertext) {
|
|
2543
2630
|
throw new Error("Encryption failed");
|
|
2544
2631
|
}
|
|
@@ -2547,7 +2634,8 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2547
2634
|
to: recipientDid,
|
|
2548
2635
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2549
2636
|
nonce: (0, import_tweetnacl_util.encodeBase64)(nonce),
|
|
2550
|
-
ciphertext_hash: await sha256((0, import_tweetnacl_util.encodeBase64)(ciphertext))
|
|
2637
|
+
ciphertext_hash: await sha256((0, import_tweetnacl_util.encodeBase64)(ciphertext)),
|
|
2638
|
+
ratchet_step: currentStep
|
|
2551
2639
|
});
|
|
2552
2640
|
const signature = import_tweetnacl.default.sign.detached((0, import_tweetnacl_util.decodeUTF8)(envelopeData), this.signingKeyPair.secretKey);
|
|
2553
2641
|
const payload = {
|
|
@@ -2623,13 +2711,85 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2623
2711
|
const decrypted = [];
|
|
2624
2712
|
for (const msg of data.messages) {
|
|
2625
2713
|
try {
|
|
2714
|
+
if (this._seenMessageIds.has(msg.id)) continue;
|
|
2626
2715
|
const senderEncPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_encryption_key);
|
|
2627
2716
|
const ciphertext = (0, import_tweetnacl_util.decodeBase64)(msg.ciphertext);
|
|
2628
2717
|
const nonce = (0, import_tweetnacl_util.decodeBase64)(msg.nonce);
|
|
2629
|
-
|
|
2630
|
-
|
|
2718
|
+
let rawPlaintext = null;
|
|
2719
|
+
let envelopeRatchetStep = 0;
|
|
2720
|
+
if (msg.envelope) {
|
|
2721
|
+
try {
|
|
2722
|
+
const env = JSON.parse(msg.envelope);
|
|
2723
|
+
if (typeof env.ratchet_step === "number") {
|
|
2724
|
+
envelopeRatchetStep = env.ratchet_step;
|
|
2725
|
+
}
|
|
2726
|
+
} catch {
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
if (envelopeRatchetStep > 0) {
|
|
2730
|
+
const pairId = `${msg.from}:${this.did}`;
|
|
2731
|
+
let state = this._ratchetStates.get(pairId);
|
|
2732
|
+
if (!state) {
|
|
2733
|
+
const sharedSecret = import_tweetnacl.default.box.before(senderEncPub, this.encryptionKeyPair.secretKey);
|
|
2734
|
+
state = {
|
|
2735
|
+
sendChainKey: sharedSecret,
|
|
2736
|
+
// Our sending chain to this peer
|
|
2737
|
+
sendStep: 0,
|
|
2738
|
+
recvChainKey: sharedSecret,
|
|
2739
|
+
// Their sending chain (our receiving)
|
|
2740
|
+
recvStep: 0,
|
|
2741
|
+
skippedKeys: /* @__PURE__ */ new Map()
|
|
2742
|
+
};
|
|
2743
|
+
this._ratchetStates.set(pairId, state);
|
|
2744
|
+
}
|
|
2745
|
+
const targetStep = envelopeRatchetStep;
|
|
2746
|
+
if (state.skippedKeys.has(targetStep)) {
|
|
2747
|
+
const mk = state.skippedKeys.get(targetStep);
|
|
2748
|
+
rawPlaintext = import_tweetnacl.default.secretbox.open(ciphertext, nonce, mk);
|
|
2749
|
+
state.skippedKeys.delete(targetStep);
|
|
2750
|
+
} else if (targetStep > state.recvStep) {
|
|
2751
|
+
const skip = targetStep - state.recvStep;
|
|
2752
|
+
if (skip > MAX_SKIP) {
|
|
2753
|
+
rawPlaintext = import_tweetnacl.default.box.open(ciphertext, nonce, senderEncPub, this.encryptionKeyPair.secretKey);
|
|
2754
|
+
} else {
|
|
2755
|
+
let ck = state.recvChainKey;
|
|
2756
|
+
for (let i = state.recvStep + 1; i < targetStep; i++) {
|
|
2757
|
+
const { nextChainKey: nextChainKey2, messageKey: skippedMk } = await ratchetStep(ck);
|
|
2758
|
+
state.skippedKeys.set(i, skippedMk);
|
|
2759
|
+
ck = nextChainKey2;
|
|
2760
|
+
if (state.skippedKeys.size > MAX_SKIP) {
|
|
2761
|
+
const oldest = state.skippedKeys.keys().next().value;
|
|
2762
|
+
if (oldest !== void 0) state.skippedKeys.delete(oldest);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
const { nextChainKey, messageKey } = await ratchetStep(ck);
|
|
2766
|
+
state.recvChainKey = nextChainKey;
|
|
2767
|
+
state.recvStep = targetStep;
|
|
2768
|
+
rawPlaintext = import_tweetnacl.default.secretbox.open(ciphertext, nonce, messageKey);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
if (!rawPlaintext) {
|
|
2772
|
+
rawPlaintext = import_tweetnacl.default.box.open(ciphertext, nonce, senderEncPub, this.encryptionKeyPair.secretKey);
|
|
2773
|
+
}
|
|
2774
|
+
} else {
|
|
2775
|
+
rawPlaintext = import_tweetnacl.default.box.open(ciphertext, nonce, senderEncPub, this.encryptionKeyPair.secretKey);
|
|
2776
|
+
}
|
|
2777
|
+
if (!rawPlaintext) {
|
|
2778
|
+
this._decryptFailCount++;
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2631
2781
|
let plaintextBytes = rawPlaintext;
|
|
2632
|
-
|
|
2782
|
+
let wasPadded = false;
|
|
2783
|
+
let wasSealed = false;
|
|
2784
|
+
const proto = parseProtoHeader(rawPlaintext);
|
|
2785
|
+
if (proto) {
|
|
2786
|
+
wasPadded = !!(proto.flags & FLAG_PADDED);
|
|
2787
|
+
wasSealed = !!(proto.flags & FLAG_SEALED);
|
|
2788
|
+
plaintextBytes = proto.content;
|
|
2789
|
+
}
|
|
2790
|
+
if (wasPadded) {
|
|
2791
|
+
plaintextBytes = unpadMessage(plaintextBytes);
|
|
2792
|
+
} else if (!proto && rawPlaintext.length >= 256 && (rawPlaintext.length & rawPlaintext.length - 1) === 0) {
|
|
2633
2793
|
const unpadded = unpadMessage(rawPlaintext);
|
|
2634
2794
|
if (unpadded.length < rawPlaintext.length) {
|
|
2635
2795
|
plaintextBytes = unpadded;
|
|
@@ -2637,10 +2797,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2637
2797
|
}
|
|
2638
2798
|
let content = (0, import_tweetnacl_util.encodeUTF8)(plaintextBytes);
|
|
2639
2799
|
let senderDid = msg.from;
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2800
|
+
if (wasSealed || !proto) {
|
|
2801
|
+
const unsealed = unsealEnvelope(content);
|
|
2802
|
+
if (unsealed) {
|
|
2803
|
+
content = unsealed.msg;
|
|
2804
|
+
senderDid = unsealed.from;
|
|
2805
|
+
}
|
|
2644
2806
|
}
|
|
2645
2807
|
let signatureValid = false;
|
|
2646
2808
|
try {
|
|
@@ -2661,6 +2823,15 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2661
2823
|
} catch {
|
|
2662
2824
|
signatureValid = false;
|
|
2663
2825
|
}
|
|
2826
|
+
if (this.requireSignatures && !signatureValid) {
|
|
2827
|
+
this._decryptFailCount++;
|
|
2828
|
+
continue;
|
|
2829
|
+
}
|
|
2830
|
+
this._seenMessageIds.add(msg.id);
|
|
2831
|
+
if (this._seenMessageIds.size > 1e4) {
|
|
2832
|
+
const first = this._seenMessageIds.values().next().value;
|
|
2833
|
+
if (first !== void 0) this._seenMessageIds.delete(first);
|
|
2834
|
+
}
|
|
2664
2835
|
decrypted.push({
|
|
2665
2836
|
id: msg.id,
|
|
2666
2837
|
from: senderDid,
|
|
@@ -2675,6 +2846,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2675
2846
|
expiresAt: msg.expires_at
|
|
2676
2847
|
});
|
|
2677
2848
|
} catch {
|
|
2849
|
+
this._decryptFailCount++;
|
|
2678
2850
|
}
|
|
2679
2851
|
}
|
|
2680
2852
|
return decrypted;
|
|
@@ -2725,9 +2897,19 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2725
2897
|
* Look up an agent's public profile and keys.
|
|
2726
2898
|
*/
|
|
2727
2899
|
async getIdentity(did) {
|
|
2900
|
+
const cached = this._identityCache.get(did);
|
|
2901
|
+
if (cached && Date.now() - cached.cachedAt < 3e5) {
|
|
2902
|
+
return cached.profile;
|
|
2903
|
+
}
|
|
2728
2904
|
const res = await fetch(`${this.baseUrl}/v1/agent/identity/${did}`);
|
|
2729
2905
|
if (!res.ok) return null;
|
|
2730
|
-
|
|
2906
|
+
const profile = await res.json();
|
|
2907
|
+
this._identityCache.set(did, { profile, cachedAt: Date.now() });
|
|
2908
|
+
if (this._identityCache.size > 500) {
|
|
2909
|
+
const oldest = this._identityCache.keys().next().value;
|
|
2910
|
+
if (oldest !== void 0) this._identityCache.delete(oldest);
|
|
2911
|
+
}
|
|
2912
|
+
return profile;
|
|
2731
2913
|
}
|
|
2732
2914
|
/**
|
|
2733
2915
|
* Search for agents by name or capability.
|
|
@@ -2787,10 +2969,14 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2787
2969
|
* Delete a webhook.
|
|
2788
2970
|
*/
|
|
2789
2971
|
async deleteWebhook(webhookId) {
|
|
2790
|
-
await fetch(`${this.baseUrl}/v1/agent/webhooks/${webhookId}`, {
|
|
2972
|
+
const res = await fetch(`${this.baseUrl}/v1/agent/webhooks/${webhookId}`, {
|
|
2791
2973
|
method: "DELETE",
|
|
2792
2974
|
headers: { "X-Agent-Key": this.apiKey }
|
|
2793
2975
|
});
|
|
2976
|
+
if (!res.ok) {
|
|
2977
|
+
const err = await res.json().catch(() => ({}));
|
|
2978
|
+
throw new Error(`Webhook delete failed: ${err.error || res.statusText}`);
|
|
2979
|
+
}
|
|
2794
2980
|
}
|
|
2795
2981
|
/**
|
|
2796
2982
|
* Verify a webhook payload signature (for use in your webhook handler).
|
|
@@ -3408,13 +3594,21 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3408
3594
|
* The relay finds matching agents and creates individual tasks for each.
|
|
3409
3595
|
*/
|
|
3410
3596
|
async broadcastTask(options) {
|
|
3597
|
+
const inputBytes = (0, import_tweetnacl_util.decodeUTF8)(options.input);
|
|
3598
|
+
const broadcastNonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.secretbox.nonceLength);
|
|
3599
|
+
const broadcastEncrypted = import_tweetnacl.default.secretbox(inputBytes, broadcastNonce, import_tweetnacl.default.box.before(
|
|
3600
|
+
this.encryptionKeyPair.publicKey,
|
|
3601
|
+
this.encryptionKeyPair.secretKey
|
|
3602
|
+
));
|
|
3603
|
+
const broadcastSig = import_tweetnacl.default.sign.detached(inputBytes, this.signingKeyPair.secretKey);
|
|
3411
3604
|
const res = await fetch(`${this.baseUrl}/v1/agent/tasks/broadcast`, {
|
|
3412
3605
|
method: "POST",
|
|
3413
3606
|
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
3414
3607
|
body: JSON.stringify({
|
|
3415
3608
|
capability: options.capability,
|
|
3416
|
-
encrypted_input: (0, import_tweetnacl_util.encodeBase64)(
|
|
3417
|
-
input_nonce: (0, import_tweetnacl_util.encodeBase64)(
|
|
3609
|
+
encrypted_input: (0, import_tweetnacl_util.encodeBase64)(broadcastEncrypted),
|
|
3610
|
+
input_nonce: (0, import_tweetnacl_util.encodeBase64)(broadcastNonce),
|
|
3611
|
+
input_signature: (0, import_tweetnacl_util.encodeBase64)(broadcastSig),
|
|
3418
3612
|
priority: options.priority,
|
|
3419
3613
|
max_agents: options.maxAgents,
|
|
3420
3614
|
min_trust_level: options.minTrustLevel,
|
|
@@ -3484,16 +3678,30 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3484
3678
|
// ─── Memory Store ──────────────────────────────────────────────────────────
|
|
3485
3679
|
/**
|
|
3486
3680
|
* Store an encrypted key-value pair in persistent memory.
|
|
3487
|
-
* Values are encrypted with
|
|
3681
|
+
* Values are encrypted CLIENT-SIDE with nacl.secretbox before sending to relay.
|
|
3682
|
+
* The relay never sees plaintext values — true E2E encrypted storage.
|
|
3488
3683
|
*/
|
|
3489
3684
|
async memorySet(namespace, key, value, options) {
|
|
3685
|
+
const valueStr = JSON.stringify(value);
|
|
3686
|
+
const valueBytes = (0, import_tweetnacl_util.decodeUTF8)(valueStr);
|
|
3687
|
+
const memNonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.secretbox.nonceLength);
|
|
3688
|
+
const memKeyInput = new Uint8Array([...this.encryptionKeyPair.secretKey, 77, 69, 77]);
|
|
3689
|
+
let memKey;
|
|
3690
|
+
if (typeof globalThis.crypto?.subtle !== "undefined") {
|
|
3691
|
+
memKey = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", memKeyInput));
|
|
3692
|
+
} else {
|
|
3693
|
+
const { createHash } = await import("crypto");
|
|
3694
|
+
memKey = new Uint8Array(createHash("sha256").update(Buffer.from(memKeyInput)).digest());
|
|
3695
|
+
}
|
|
3696
|
+
const encryptedValue = import_tweetnacl.default.secretbox(valueBytes, memNonce, memKey);
|
|
3490
3697
|
const res = await fetch(`${this.baseUrl}/v1/agent/memory/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`, {
|
|
3491
3698
|
method: "PUT",
|
|
3492
3699
|
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
3493
3700
|
body: JSON.stringify({
|
|
3494
|
-
value,
|
|
3495
|
-
value_type: options?.valueType || (typeof value === "object" ? "json" : typeof value)
|
|
3496
|
-
ttl: options?.ttl
|
|
3701
|
+
value: (0, import_tweetnacl_util.encodeBase64)(encryptedValue),
|
|
3702
|
+
value_type: `encrypted:${options?.valueType || (typeof value === "object" ? "json" : typeof value)}`,
|
|
3703
|
+
ttl: options?.ttl,
|
|
3704
|
+
client_nonce: (0, import_tweetnacl_util.encodeBase64)(memNonce)
|
|
3497
3705
|
})
|
|
3498
3706
|
});
|
|
3499
3707
|
if (!res.ok) throw new Error(`Memory set failed: ${res.status} ${await res.text()}`);
|
|
@@ -3501,7 +3709,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3501
3709
|
}
|
|
3502
3710
|
/**
|
|
3503
3711
|
* Retrieve a value from persistent memory.
|
|
3504
|
-
* Decrypted
|
|
3712
|
+
* Decrypted CLIENT-SIDE — relay never sees plaintext.
|
|
3505
3713
|
*/
|
|
3506
3714
|
async memoryGet(namespace, key) {
|
|
3507
3715
|
const res = await fetch(`${this.baseUrl}/v1/agent/memory/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`, {
|
|
@@ -3509,7 +3717,28 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3509
3717
|
});
|
|
3510
3718
|
if (res.status === 404) return null;
|
|
3511
3719
|
if (!res.ok) throw new Error(`Memory get failed: ${res.status} ${await res.text()}`);
|
|
3512
|
-
|
|
3720
|
+
const data = await res.json();
|
|
3721
|
+
if (data.value_type?.startsWith("encrypted:") && data.client_nonce) {
|
|
3722
|
+
try {
|
|
3723
|
+
const memKeyInput = new Uint8Array([...this.encryptionKeyPair.secretKey, 77, 69, 77]);
|
|
3724
|
+
let memKey;
|
|
3725
|
+
if (typeof globalThis.crypto?.subtle !== "undefined") {
|
|
3726
|
+
memKey = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", memKeyInput));
|
|
3727
|
+
} else {
|
|
3728
|
+
const { createHash } = await import("crypto");
|
|
3729
|
+
memKey = new Uint8Array(createHash("sha256").update(Buffer.from(memKeyInput)).digest());
|
|
3730
|
+
}
|
|
3731
|
+
const encBytes = (0, import_tweetnacl_util.decodeBase64)(data.value);
|
|
3732
|
+
const memNonce = (0, import_tweetnacl_util.decodeBase64)(data.client_nonce);
|
|
3733
|
+
const plain = import_tweetnacl.default.secretbox.open(encBytes, memNonce, memKey);
|
|
3734
|
+
if (plain) {
|
|
3735
|
+
data.value = JSON.parse((0, import_tweetnacl_util.encodeUTF8)(plain));
|
|
3736
|
+
data.value_type = data.value_type.replace("encrypted:", "");
|
|
3737
|
+
}
|
|
3738
|
+
} catch {
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
return data;
|
|
3513
3742
|
}
|
|
3514
3743
|
/**
|
|
3515
3744
|
* Delete a key from persistent memory.
|
|
@@ -3978,31 +4207,40 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3978
4207
|
"Approximate message size (even with padding, bounded to power-of-2)"
|
|
3979
4208
|
],
|
|
3980
4209
|
relayCannotSee: [
|
|
3981
|
-
"Message content (E2E encrypted with
|
|
4210
|
+
"Message content (E2E encrypted \u2014 nacl.secretbox with ratchet-derived per-message keys)",
|
|
3982
4211
|
"Private keys (generated and stored client-side only)",
|
|
3983
|
-
"Memory values (encrypted with
|
|
3984
|
-
"
|
|
4212
|
+
"Memory values (encrypted CLIENT-SIDE with nacl.secretbox before relay storage)",
|
|
4213
|
+
"Past message keys (hash ratchet provides forward secrecy \u2014 old keys are deleted)",
|
|
3985
4214
|
...this.sealedSender ? ["Sender identity (sealed inside ciphertext)"] : []
|
|
3986
4215
|
],
|
|
3987
4216
|
protections: [
|
|
4217
|
+
"Hash ratchet forward secrecy \u2014 per-message key derivation, old keys deleted",
|
|
3988
4218
|
"X25519 key exchange + XSalsa20-Poly1305 authenticated encryption",
|
|
3989
|
-
"Ed25519 signatures on every message",
|
|
4219
|
+
"Ed25519 signatures on every message (envelope + ciphertext hash)",
|
|
3990
4220
|
"TOFU key pinning (MitM detection on key change)",
|
|
4221
|
+
"Client-side memory encryption (relay never sees plaintext values)",
|
|
4222
|
+
"Protocol version header (deterministic padding/sealing detection, no heuristics)",
|
|
4223
|
+
"Identity cache (reduced key lookups, 5-min TTL)",
|
|
4224
|
+
"Message deduplication (track seen message IDs)",
|
|
4225
|
+
"Request validation (fromCredentials validates key sizes and format)",
|
|
3991
4226
|
...this.paddingEnabled ? ["Message padding to power-of-2 boundary (traffic analysis resistance)"] : [],
|
|
3992
4227
|
...this.sealedSender ? ["Sealed sender (relay cannot see who sent a message)"] : [],
|
|
4228
|
+
...this.requireSignatures ? ["Strict signature enforcement (reject unsigned/invalid messages)"] : [],
|
|
3993
4229
|
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays)`] : [],
|
|
3994
4230
|
"Auto-retry with exponential backoff",
|
|
3995
|
-
"Offline message queue"
|
|
4231
|
+
"Offline message queue",
|
|
4232
|
+
"did:key interoperability (W3C standard DID format)"
|
|
3996
4233
|
],
|
|
3997
4234
|
gaps: [
|
|
3998
|
-
"No
|
|
3999
|
-
"No post-quantum protection \u2014 vulnerable to harvest-now-decrypt-later",
|
|
4235
|
+
"No DH ratchet yet \u2014 hash ratchet only (no post-compromise recovery, planned for v3)",
|
|
4236
|
+
"No post-quantum protection \u2014 vulnerable to harvest-now-decrypt-later (planned for v3)",
|
|
4000
4237
|
"Channel encryption is server-side (relay holds channel keys, NOT true E2E)",
|
|
4001
4238
|
"Metadata (who, when, thread structure) visible to relay operator",
|
|
4002
4239
|
"Single relay architecture (no onion routing, no mix network)",
|
|
4003
4240
|
"Ed25519 signatures are non-repudiable (no deniable messaging)",
|
|
4004
4241
|
"No async key agreement (no X3DH prekeys)",
|
|
4005
|
-
"Polling-based (no WebSocket real-time transport)"
|
|
4242
|
+
"Polling-based (no WebSocket real-time transport)",
|
|
4243
|
+
"Ratchet state is in-memory (lost on process restart \u2014 export credentials to persist)"
|
|
4006
4244
|
]
|
|
4007
4245
|
};
|
|
4008
4246
|
}
|
|
@@ -4028,6 +4266,9 @@ var Conversation = class {
|
|
|
4028
4266
|
replyTo: this._lastMessageId || void 0
|
|
4029
4267
|
});
|
|
4030
4268
|
this._lastMessageId = result.id;
|
|
4269
|
+
if (this._messageHistory.length >= 1e3) {
|
|
4270
|
+
this._messageHistory.splice(0, this._messageHistory.length - 999);
|
|
4271
|
+
}
|
|
4031
4272
|
this._messageHistory.push({
|
|
4032
4273
|
id: result.id,
|
|
4033
4274
|
from: this.agent.did,
|
|
@@ -4102,14 +4343,23 @@ var Conversation = class {
|
|
|
4102
4343
|
async waitForReply(timeoutMs = 3e4) {
|
|
4103
4344
|
return new Promise((resolve, reject) => {
|
|
4104
4345
|
let resolved = false;
|
|
4346
|
+
let pollTimer = null;
|
|
4347
|
+
const cleanup = () => {
|
|
4348
|
+
resolved = true;
|
|
4349
|
+
if (pollTimer) {
|
|
4350
|
+
clearTimeout(pollTimer);
|
|
4351
|
+
pollTimer = null;
|
|
4352
|
+
}
|
|
4353
|
+
};
|
|
4105
4354
|
const timeout = setTimeout(() => {
|
|
4106
4355
|
if (!resolved) {
|
|
4107
|
-
|
|
4356
|
+
cleanup();
|
|
4108
4357
|
reject(new Error(`No reply received within ${timeoutMs}ms`));
|
|
4109
4358
|
}
|
|
4110
4359
|
}, timeoutMs);
|
|
4111
4360
|
const check = async () => {
|
|
4112
|
-
|
|
4361
|
+
if (resolved) return;
|
|
4362
|
+
try {
|
|
4113
4363
|
const messages = await this.agent.receive({
|
|
4114
4364
|
from: this.peerDid,
|
|
4115
4365
|
threadId: this.threadId,
|
|
@@ -4117,9 +4367,12 @@ var Conversation = class {
|
|
|
4117
4367
|
limit: 1
|
|
4118
4368
|
});
|
|
4119
4369
|
if (messages.length > 0 && !resolved) {
|
|
4120
|
-
resolved = true;
|
|
4121
4370
|
clearTimeout(timeout);
|
|
4371
|
+
cleanup();
|
|
4122
4372
|
const msg = messages[0];
|
|
4373
|
+
if (this._messageHistory.length >= 1e3) {
|
|
4374
|
+
this._messageHistory.splice(0, this._messageHistory.length - 999);
|
|
4375
|
+
}
|
|
4123
4376
|
this._messageHistory.push({
|
|
4124
4377
|
id: msg.id,
|
|
4125
4378
|
from: msg.from,
|
|
@@ -4134,16 +4387,19 @@ var Conversation = class {
|
|
|
4134
4387
|
resolve(msg);
|
|
4135
4388
|
return;
|
|
4136
4389
|
}
|
|
4137
|
-
|
|
4390
|
+
} catch (err) {
|
|
4391
|
+
if (!resolved) {
|
|
4392
|
+
clearTimeout(timeout);
|
|
4393
|
+
cleanup();
|
|
4394
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
4138
4397
|
}
|
|
4139
|
-
};
|
|
4140
|
-
check().catch((err) => {
|
|
4141
4398
|
if (!resolved) {
|
|
4142
|
-
|
|
4143
|
-
clearTimeout(timeout);
|
|
4144
|
-
reject(err);
|
|
4399
|
+
pollTimer = setTimeout(check, 1500);
|
|
4145
4400
|
}
|
|
4146
|
-
}
|
|
4401
|
+
};
|
|
4402
|
+
check();
|
|
4147
4403
|
});
|
|
4148
4404
|
}
|
|
4149
4405
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -2394,6 +2394,38 @@ function unsealEnvelope(plaintext) {
|
|
|
2394
2394
|
return null;
|
|
2395
2395
|
}
|
|
2396
2396
|
}
|
|
2397
|
+
var PROTO_MARKER = 86;
|
|
2398
|
+
var FLAG_PADDED = 1;
|
|
2399
|
+
var FLAG_SEALED = 2;
|
|
2400
|
+
var FLAG_RATCHET = 4;
|
|
2401
|
+
function makeProtoHeader(flags, ratchetStep2) {
|
|
2402
|
+
return new Uint8Array([PROTO_MARKER, flags, ratchetStep2 >> 8 & 255, ratchetStep2 & 255]);
|
|
2403
|
+
}
|
|
2404
|
+
function parseProtoHeader(data) {
|
|
2405
|
+
if (data.length < 4 || data[0] !== PROTO_MARKER) return null;
|
|
2406
|
+
return {
|
|
2407
|
+
flags: data[1],
|
|
2408
|
+
ratchetStep: data[2] << 8 | data[3],
|
|
2409
|
+
content: data.slice(4)
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
var MAX_SKIP = 200;
|
|
2413
|
+
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
2414
|
+
function toBase58(bytes) {
|
|
2415
|
+
if (bytes.length === 0) return "1";
|
|
2416
|
+
let result = "";
|
|
2417
|
+
let num = BigInt("0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""));
|
|
2418
|
+
while (num > 0n) {
|
|
2419
|
+
const remainder = num % 58n;
|
|
2420
|
+
num = num / 58n;
|
|
2421
|
+
result = BASE58_ALPHABET[Number(remainder)] + result;
|
|
2422
|
+
}
|
|
2423
|
+
for (const byte of bytes) {
|
|
2424
|
+
if (byte === 0) result = "1" + result;
|
|
2425
|
+
else break;
|
|
2426
|
+
}
|
|
2427
|
+
return result || "1";
|
|
2428
|
+
}
|
|
2397
2429
|
var VoidlyAgent = class _VoidlyAgent {
|
|
2398
2430
|
constructor(identity, config) {
|
|
2399
2431
|
this._pinnedDids = /* @__PURE__ */ new Set();
|
|
@@ -2401,6 +2433,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2401
2433
|
this._conversations = /* @__PURE__ */ new Map();
|
|
2402
2434
|
this._offlineQueue = [];
|
|
2403
2435
|
this._ratchetStates = /* @__PURE__ */ new Map();
|
|
2436
|
+
this._identityCache = /* @__PURE__ */ new Map();
|
|
2437
|
+
this._seenMessageIds = /* @__PURE__ */ new Set();
|
|
2438
|
+
this._decryptFailCount = 0;
|
|
2404
2439
|
this.did = identity.did;
|
|
2405
2440
|
this.apiKey = identity.apiKey;
|
|
2406
2441
|
this.signingKeyPair = identity.signingKeyPair;
|
|
@@ -2411,6 +2446,8 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2411
2446
|
this.fallbackRelays = config?.fallbackRelays || [];
|
|
2412
2447
|
this.paddingEnabled = config?.padding !== false;
|
|
2413
2448
|
this.sealedSender = config?.sealedSender || false;
|
|
2449
|
+
this.requireSignatures = config?.requireSignatures || false;
|
|
2450
|
+
this.timeout = config?.timeout ?? 3e4;
|
|
2414
2451
|
}
|
|
2415
2452
|
// ─── Factory Methods ────────────────────────────────────────────────────────
|
|
2416
2453
|
/**
|
|
@@ -2449,8 +2486,29 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2449
2486
|
* Use this to resume an agent across sessions.
|
|
2450
2487
|
*/
|
|
2451
2488
|
static fromCredentials(creds, config) {
|
|
2452
|
-
|
|
2453
|
-
|
|
2489
|
+
if (!creds.did || !creds.did.startsWith("did:")) {
|
|
2490
|
+
throw new Error('Invalid credentials: did must start with "did:"');
|
|
2491
|
+
}
|
|
2492
|
+
if (!creds.apiKey || creds.apiKey.length < 8) {
|
|
2493
|
+
throw new Error("Invalid credentials: apiKey is missing or too short");
|
|
2494
|
+
}
|
|
2495
|
+
if (!creds.signingSecretKey || !creds.encryptionSecretKey) {
|
|
2496
|
+
throw new Error("Invalid credentials: secret keys are required");
|
|
2497
|
+
}
|
|
2498
|
+
let signingSecret;
|
|
2499
|
+
let encryptionSecret;
|
|
2500
|
+
try {
|
|
2501
|
+
signingSecret = (0, import_tweetnacl_util.decodeBase64)(creds.signingSecretKey);
|
|
2502
|
+
encryptionSecret = (0, import_tweetnacl_util.decodeBase64)(creds.encryptionSecretKey);
|
|
2503
|
+
} catch {
|
|
2504
|
+
throw new Error("Invalid credentials: secret keys must be valid base64");
|
|
2505
|
+
}
|
|
2506
|
+
if (signingSecret.length !== 64) {
|
|
2507
|
+
throw new Error(`Invalid credentials: signing key must be 64 bytes, got ${signingSecret.length}`);
|
|
2508
|
+
}
|
|
2509
|
+
if (encryptionSecret.length !== 32) {
|
|
2510
|
+
throw new Error(`Invalid credentials: encryption key must be 32 bytes, got ${encryptionSecret.length}`);
|
|
2511
|
+
}
|
|
2454
2512
|
return new _VoidlyAgent({
|
|
2455
2513
|
did: creds.did,
|
|
2456
2514
|
apiKey: creds.apiKey,
|
|
@@ -2475,6 +2533,25 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2475
2533
|
encryptionPublicKey: (0, import_tweetnacl_util.encodeBase64)(this.encryptionKeyPair.publicKey)
|
|
2476
2534
|
};
|
|
2477
2535
|
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Get the number of messages that failed to decrypt.
|
|
2538
|
+
* Useful for detecting key mismatches, attacks, or corruption.
|
|
2539
|
+
*/
|
|
2540
|
+
get decryptFailCount() {
|
|
2541
|
+
return this._decryptFailCount;
|
|
2542
|
+
}
|
|
2543
|
+
/**
|
|
2544
|
+
* Generate a did:key identifier from this agent's Ed25519 signing key.
|
|
2545
|
+
* did:key is a W3C standard — interoperable across systems.
|
|
2546
|
+
* Format: did:key:z6Mk{base58-multicodec-ed25519-pubkey}
|
|
2547
|
+
*/
|
|
2548
|
+
get didKey() {
|
|
2549
|
+
const multicodec = new Uint8Array(2 + this.signingKeyPair.publicKey.length);
|
|
2550
|
+
multicodec[0] = 237;
|
|
2551
|
+
multicodec[1] = 1;
|
|
2552
|
+
multicodec.set(this.signingKeyPair.publicKey, 2);
|
|
2553
|
+
return `did:key:z${toBase58(multicodec)}`;
|
|
2554
|
+
}
|
|
2478
2555
|
// ─── Messaging ──────────────────────────────────────────────────────────────
|
|
2479
2556
|
/**
|
|
2480
2557
|
* Send an E2E encrypted message with hardened security.
|
|
@@ -2505,29 +2582,39 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2505
2582
|
if (useSealed) {
|
|
2506
2583
|
plaintext = sealEnvelope(this.did, message);
|
|
2507
2584
|
}
|
|
2508
|
-
let
|
|
2585
|
+
let contentBytes;
|
|
2509
2586
|
if (usePadding) {
|
|
2510
|
-
|
|
2587
|
+
contentBytes = padMessage((0, import_tweetnacl_util.decodeUTF8)(plaintext));
|
|
2511
2588
|
} else {
|
|
2512
|
-
|
|
2589
|
+
contentBytes = (0, import_tweetnacl_util.decodeUTF8)(plaintext);
|
|
2513
2590
|
}
|
|
2514
2591
|
const pairId = `${this.did}:${recipientDid}`;
|
|
2515
|
-
|
|
2516
|
-
|
|
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 {
|
|
2592
|
+
let state = this._ratchetStates.get(pairId);
|
|
2593
|
+
if (!state) {
|
|
2523
2594
|
const sharedSecret = import_tweetnacl.default.box.before(recipientPubKey, this.encryptionKeyPair.secretKey);
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2595
|
+
state = {
|
|
2596
|
+
sendChainKey: sharedSecret,
|
|
2597
|
+
sendStep: 0,
|
|
2598
|
+
recvChainKey: sharedSecret,
|
|
2599
|
+
// Will be synced on first receive
|
|
2600
|
+
recvStep: 0,
|
|
2601
|
+
skippedKeys: /* @__PURE__ */ new Map()
|
|
2602
|
+
};
|
|
2603
|
+
this._ratchetStates.set(pairId, state);
|
|
2528
2604
|
}
|
|
2529
|
-
const
|
|
2530
|
-
|
|
2605
|
+
const { nextChainKey, messageKey } = await ratchetStep(state.sendChainKey);
|
|
2606
|
+
state.sendChainKey = nextChainKey;
|
|
2607
|
+
state.sendStep++;
|
|
2608
|
+
const currentStep = state.sendStep;
|
|
2609
|
+
let flags = FLAG_RATCHET;
|
|
2610
|
+
if (usePadding) flags |= FLAG_PADDED;
|
|
2611
|
+
if (useSealed) flags |= FLAG_SEALED;
|
|
2612
|
+
const header = makeProtoHeader(flags, currentStep);
|
|
2613
|
+
const messageBytes = new Uint8Array(header.length + contentBytes.length);
|
|
2614
|
+
messageBytes.set(header, 0);
|
|
2615
|
+
messageBytes.set(contentBytes, header.length);
|
|
2616
|
+
const nonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.secretbox.nonceLength);
|
|
2617
|
+
const ciphertext = import_tweetnacl.default.secretbox(messageBytes, nonce, messageKey);
|
|
2531
2618
|
if (!ciphertext) {
|
|
2532
2619
|
throw new Error("Encryption failed");
|
|
2533
2620
|
}
|
|
@@ -2536,7 +2623,8 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2536
2623
|
to: recipientDid,
|
|
2537
2624
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2538
2625
|
nonce: (0, import_tweetnacl_util.encodeBase64)(nonce),
|
|
2539
|
-
ciphertext_hash: await sha256((0, import_tweetnacl_util.encodeBase64)(ciphertext))
|
|
2626
|
+
ciphertext_hash: await sha256((0, import_tweetnacl_util.encodeBase64)(ciphertext)),
|
|
2627
|
+
ratchet_step: currentStep
|
|
2540
2628
|
});
|
|
2541
2629
|
const signature = import_tweetnacl.default.sign.detached((0, import_tweetnacl_util.decodeUTF8)(envelopeData), this.signingKeyPair.secretKey);
|
|
2542
2630
|
const payload = {
|
|
@@ -2612,13 +2700,85 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2612
2700
|
const decrypted = [];
|
|
2613
2701
|
for (const msg of data.messages) {
|
|
2614
2702
|
try {
|
|
2703
|
+
if (this._seenMessageIds.has(msg.id)) continue;
|
|
2615
2704
|
const senderEncPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_encryption_key);
|
|
2616
2705
|
const ciphertext = (0, import_tweetnacl_util.decodeBase64)(msg.ciphertext);
|
|
2617
2706
|
const nonce = (0, import_tweetnacl_util.decodeBase64)(msg.nonce);
|
|
2618
|
-
|
|
2619
|
-
|
|
2707
|
+
let rawPlaintext = null;
|
|
2708
|
+
let envelopeRatchetStep = 0;
|
|
2709
|
+
if (msg.envelope) {
|
|
2710
|
+
try {
|
|
2711
|
+
const env = JSON.parse(msg.envelope);
|
|
2712
|
+
if (typeof env.ratchet_step === "number") {
|
|
2713
|
+
envelopeRatchetStep = env.ratchet_step;
|
|
2714
|
+
}
|
|
2715
|
+
} catch {
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
if (envelopeRatchetStep > 0) {
|
|
2719
|
+
const pairId = `${msg.from}:${this.did}`;
|
|
2720
|
+
let state = this._ratchetStates.get(pairId);
|
|
2721
|
+
if (!state) {
|
|
2722
|
+
const sharedSecret = import_tweetnacl.default.box.before(senderEncPub, this.encryptionKeyPair.secretKey);
|
|
2723
|
+
state = {
|
|
2724
|
+
sendChainKey: sharedSecret,
|
|
2725
|
+
// Our sending chain to this peer
|
|
2726
|
+
sendStep: 0,
|
|
2727
|
+
recvChainKey: sharedSecret,
|
|
2728
|
+
// Their sending chain (our receiving)
|
|
2729
|
+
recvStep: 0,
|
|
2730
|
+
skippedKeys: /* @__PURE__ */ new Map()
|
|
2731
|
+
};
|
|
2732
|
+
this._ratchetStates.set(pairId, state);
|
|
2733
|
+
}
|
|
2734
|
+
const targetStep = envelopeRatchetStep;
|
|
2735
|
+
if (state.skippedKeys.has(targetStep)) {
|
|
2736
|
+
const mk = state.skippedKeys.get(targetStep);
|
|
2737
|
+
rawPlaintext = import_tweetnacl.default.secretbox.open(ciphertext, nonce, mk);
|
|
2738
|
+
state.skippedKeys.delete(targetStep);
|
|
2739
|
+
} else if (targetStep > state.recvStep) {
|
|
2740
|
+
const skip = targetStep - state.recvStep;
|
|
2741
|
+
if (skip > MAX_SKIP) {
|
|
2742
|
+
rawPlaintext = import_tweetnacl.default.box.open(ciphertext, nonce, senderEncPub, this.encryptionKeyPair.secretKey);
|
|
2743
|
+
} else {
|
|
2744
|
+
let ck = state.recvChainKey;
|
|
2745
|
+
for (let i = state.recvStep + 1; i < targetStep; i++) {
|
|
2746
|
+
const { nextChainKey: nextChainKey2, messageKey: skippedMk } = await ratchetStep(ck);
|
|
2747
|
+
state.skippedKeys.set(i, skippedMk);
|
|
2748
|
+
ck = nextChainKey2;
|
|
2749
|
+
if (state.skippedKeys.size > MAX_SKIP) {
|
|
2750
|
+
const oldest = state.skippedKeys.keys().next().value;
|
|
2751
|
+
if (oldest !== void 0) state.skippedKeys.delete(oldest);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
const { nextChainKey, messageKey } = await ratchetStep(ck);
|
|
2755
|
+
state.recvChainKey = nextChainKey;
|
|
2756
|
+
state.recvStep = targetStep;
|
|
2757
|
+
rawPlaintext = import_tweetnacl.default.secretbox.open(ciphertext, nonce, messageKey);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
if (!rawPlaintext) {
|
|
2761
|
+
rawPlaintext = import_tweetnacl.default.box.open(ciphertext, nonce, senderEncPub, this.encryptionKeyPair.secretKey);
|
|
2762
|
+
}
|
|
2763
|
+
} else {
|
|
2764
|
+
rawPlaintext = import_tweetnacl.default.box.open(ciphertext, nonce, senderEncPub, this.encryptionKeyPair.secretKey);
|
|
2765
|
+
}
|
|
2766
|
+
if (!rawPlaintext) {
|
|
2767
|
+
this._decryptFailCount++;
|
|
2768
|
+
continue;
|
|
2769
|
+
}
|
|
2620
2770
|
let plaintextBytes = rawPlaintext;
|
|
2621
|
-
|
|
2771
|
+
let wasPadded = false;
|
|
2772
|
+
let wasSealed = false;
|
|
2773
|
+
const proto = parseProtoHeader(rawPlaintext);
|
|
2774
|
+
if (proto) {
|
|
2775
|
+
wasPadded = !!(proto.flags & FLAG_PADDED);
|
|
2776
|
+
wasSealed = !!(proto.flags & FLAG_SEALED);
|
|
2777
|
+
plaintextBytes = proto.content;
|
|
2778
|
+
}
|
|
2779
|
+
if (wasPadded) {
|
|
2780
|
+
plaintextBytes = unpadMessage(plaintextBytes);
|
|
2781
|
+
} else if (!proto && rawPlaintext.length >= 256 && (rawPlaintext.length & rawPlaintext.length - 1) === 0) {
|
|
2622
2782
|
const unpadded = unpadMessage(rawPlaintext);
|
|
2623
2783
|
if (unpadded.length < rawPlaintext.length) {
|
|
2624
2784
|
plaintextBytes = unpadded;
|
|
@@ -2626,10 +2786,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2626
2786
|
}
|
|
2627
2787
|
let content = (0, import_tweetnacl_util.encodeUTF8)(plaintextBytes);
|
|
2628
2788
|
let senderDid = msg.from;
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2789
|
+
if (wasSealed || !proto) {
|
|
2790
|
+
const unsealed = unsealEnvelope(content);
|
|
2791
|
+
if (unsealed) {
|
|
2792
|
+
content = unsealed.msg;
|
|
2793
|
+
senderDid = unsealed.from;
|
|
2794
|
+
}
|
|
2633
2795
|
}
|
|
2634
2796
|
let signatureValid = false;
|
|
2635
2797
|
try {
|
|
@@ -2650,6 +2812,15 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2650
2812
|
} catch {
|
|
2651
2813
|
signatureValid = false;
|
|
2652
2814
|
}
|
|
2815
|
+
if (this.requireSignatures && !signatureValid) {
|
|
2816
|
+
this._decryptFailCount++;
|
|
2817
|
+
continue;
|
|
2818
|
+
}
|
|
2819
|
+
this._seenMessageIds.add(msg.id);
|
|
2820
|
+
if (this._seenMessageIds.size > 1e4) {
|
|
2821
|
+
const first = this._seenMessageIds.values().next().value;
|
|
2822
|
+
if (first !== void 0) this._seenMessageIds.delete(first);
|
|
2823
|
+
}
|
|
2653
2824
|
decrypted.push({
|
|
2654
2825
|
id: msg.id,
|
|
2655
2826
|
from: senderDid,
|
|
@@ -2664,6 +2835,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2664
2835
|
expiresAt: msg.expires_at
|
|
2665
2836
|
});
|
|
2666
2837
|
} catch {
|
|
2838
|
+
this._decryptFailCount++;
|
|
2667
2839
|
}
|
|
2668
2840
|
}
|
|
2669
2841
|
return decrypted;
|
|
@@ -2714,9 +2886,19 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2714
2886
|
* Look up an agent's public profile and keys.
|
|
2715
2887
|
*/
|
|
2716
2888
|
async getIdentity(did) {
|
|
2889
|
+
const cached = this._identityCache.get(did);
|
|
2890
|
+
if (cached && Date.now() - cached.cachedAt < 3e5) {
|
|
2891
|
+
return cached.profile;
|
|
2892
|
+
}
|
|
2717
2893
|
const res = await fetch(`${this.baseUrl}/v1/agent/identity/${did}`);
|
|
2718
2894
|
if (!res.ok) return null;
|
|
2719
|
-
|
|
2895
|
+
const profile = await res.json();
|
|
2896
|
+
this._identityCache.set(did, { profile, cachedAt: Date.now() });
|
|
2897
|
+
if (this._identityCache.size > 500) {
|
|
2898
|
+
const oldest = this._identityCache.keys().next().value;
|
|
2899
|
+
if (oldest !== void 0) this._identityCache.delete(oldest);
|
|
2900
|
+
}
|
|
2901
|
+
return profile;
|
|
2720
2902
|
}
|
|
2721
2903
|
/**
|
|
2722
2904
|
* Search for agents by name or capability.
|
|
@@ -2776,10 +2958,14 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2776
2958
|
* Delete a webhook.
|
|
2777
2959
|
*/
|
|
2778
2960
|
async deleteWebhook(webhookId) {
|
|
2779
|
-
await fetch(`${this.baseUrl}/v1/agent/webhooks/${webhookId}`, {
|
|
2961
|
+
const res = await fetch(`${this.baseUrl}/v1/agent/webhooks/${webhookId}`, {
|
|
2780
2962
|
method: "DELETE",
|
|
2781
2963
|
headers: { "X-Agent-Key": this.apiKey }
|
|
2782
2964
|
});
|
|
2965
|
+
if (!res.ok) {
|
|
2966
|
+
const err = await res.json().catch(() => ({}));
|
|
2967
|
+
throw new Error(`Webhook delete failed: ${err.error || res.statusText}`);
|
|
2968
|
+
}
|
|
2783
2969
|
}
|
|
2784
2970
|
/**
|
|
2785
2971
|
* Verify a webhook payload signature (for use in your webhook handler).
|
|
@@ -3397,13 +3583,21 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3397
3583
|
* The relay finds matching agents and creates individual tasks for each.
|
|
3398
3584
|
*/
|
|
3399
3585
|
async broadcastTask(options) {
|
|
3586
|
+
const inputBytes = (0, import_tweetnacl_util.decodeUTF8)(options.input);
|
|
3587
|
+
const broadcastNonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.secretbox.nonceLength);
|
|
3588
|
+
const broadcastEncrypted = import_tweetnacl.default.secretbox(inputBytes, broadcastNonce, import_tweetnacl.default.box.before(
|
|
3589
|
+
this.encryptionKeyPair.publicKey,
|
|
3590
|
+
this.encryptionKeyPair.secretKey
|
|
3591
|
+
));
|
|
3592
|
+
const broadcastSig = import_tweetnacl.default.sign.detached(inputBytes, this.signingKeyPair.secretKey);
|
|
3400
3593
|
const res = await fetch(`${this.baseUrl}/v1/agent/tasks/broadcast`, {
|
|
3401
3594
|
method: "POST",
|
|
3402
3595
|
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
3403
3596
|
body: JSON.stringify({
|
|
3404
3597
|
capability: options.capability,
|
|
3405
|
-
encrypted_input: (0, import_tweetnacl_util.encodeBase64)(
|
|
3406
|
-
input_nonce: (0, import_tweetnacl_util.encodeBase64)(
|
|
3598
|
+
encrypted_input: (0, import_tweetnacl_util.encodeBase64)(broadcastEncrypted),
|
|
3599
|
+
input_nonce: (0, import_tweetnacl_util.encodeBase64)(broadcastNonce),
|
|
3600
|
+
input_signature: (0, import_tweetnacl_util.encodeBase64)(broadcastSig),
|
|
3407
3601
|
priority: options.priority,
|
|
3408
3602
|
max_agents: options.maxAgents,
|
|
3409
3603
|
min_trust_level: options.minTrustLevel,
|
|
@@ -3473,16 +3667,30 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3473
3667
|
// ─── Memory Store ──────────────────────────────────────────────────────────
|
|
3474
3668
|
/**
|
|
3475
3669
|
* Store an encrypted key-value pair in persistent memory.
|
|
3476
|
-
* Values are encrypted with
|
|
3670
|
+
* Values are encrypted CLIENT-SIDE with nacl.secretbox before sending to relay.
|
|
3671
|
+
* The relay never sees plaintext values — true E2E encrypted storage.
|
|
3477
3672
|
*/
|
|
3478
3673
|
async memorySet(namespace, key, value, options) {
|
|
3674
|
+
const valueStr = JSON.stringify(value);
|
|
3675
|
+
const valueBytes = (0, import_tweetnacl_util.decodeUTF8)(valueStr);
|
|
3676
|
+
const memNonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.secretbox.nonceLength);
|
|
3677
|
+
const memKeyInput = new Uint8Array([...this.encryptionKeyPair.secretKey, 77, 69, 77]);
|
|
3678
|
+
let memKey;
|
|
3679
|
+
if (typeof globalThis.crypto?.subtle !== "undefined") {
|
|
3680
|
+
memKey = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", memKeyInput));
|
|
3681
|
+
} else {
|
|
3682
|
+
const { createHash } = await import("crypto");
|
|
3683
|
+
memKey = new Uint8Array(createHash("sha256").update(Buffer.from(memKeyInput)).digest());
|
|
3684
|
+
}
|
|
3685
|
+
const encryptedValue = import_tweetnacl.default.secretbox(valueBytes, memNonce, memKey);
|
|
3479
3686
|
const res = await fetch(`${this.baseUrl}/v1/agent/memory/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`, {
|
|
3480
3687
|
method: "PUT",
|
|
3481
3688
|
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
3482
3689
|
body: JSON.stringify({
|
|
3483
|
-
value,
|
|
3484
|
-
value_type: options?.valueType || (typeof value === "object" ? "json" : typeof value)
|
|
3485
|
-
ttl: options?.ttl
|
|
3690
|
+
value: (0, import_tweetnacl_util.encodeBase64)(encryptedValue),
|
|
3691
|
+
value_type: `encrypted:${options?.valueType || (typeof value === "object" ? "json" : typeof value)}`,
|
|
3692
|
+
ttl: options?.ttl,
|
|
3693
|
+
client_nonce: (0, import_tweetnacl_util.encodeBase64)(memNonce)
|
|
3486
3694
|
})
|
|
3487
3695
|
});
|
|
3488
3696
|
if (!res.ok) throw new Error(`Memory set failed: ${res.status} ${await res.text()}`);
|
|
@@ -3490,7 +3698,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3490
3698
|
}
|
|
3491
3699
|
/**
|
|
3492
3700
|
* Retrieve a value from persistent memory.
|
|
3493
|
-
* Decrypted
|
|
3701
|
+
* Decrypted CLIENT-SIDE — relay never sees plaintext.
|
|
3494
3702
|
*/
|
|
3495
3703
|
async memoryGet(namespace, key) {
|
|
3496
3704
|
const res = await fetch(`${this.baseUrl}/v1/agent/memory/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`, {
|
|
@@ -3498,7 +3706,28 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3498
3706
|
});
|
|
3499
3707
|
if (res.status === 404) return null;
|
|
3500
3708
|
if (!res.ok) throw new Error(`Memory get failed: ${res.status} ${await res.text()}`);
|
|
3501
|
-
|
|
3709
|
+
const data = await res.json();
|
|
3710
|
+
if (data.value_type?.startsWith("encrypted:") && data.client_nonce) {
|
|
3711
|
+
try {
|
|
3712
|
+
const memKeyInput = new Uint8Array([...this.encryptionKeyPair.secretKey, 77, 69, 77]);
|
|
3713
|
+
let memKey;
|
|
3714
|
+
if (typeof globalThis.crypto?.subtle !== "undefined") {
|
|
3715
|
+
memKey = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", memKeyInput));
|
|
3716
|
+
} else {
|
|
3717
|
+
const { createHash } = await import("crypto");
|
|
3718
|
+
memKey = new Uint8Array(createHash("sha256").update(Buffer.from(memKeyInput)).digest());
|
|
3719
|
+
}
|
|
3720
|
+
const encBytes = (0, import_tweetnacl_util.decodeBase64)(data.value);
|
|
3721
|
+
const memNonce = (0, import_tweetnacl_util.decodeBase64)(data.client_nonce);
|
|
3722
|
+
const plain = import_tweetnacl.default.secretbox.open(encBytes, memNonce, memKey);
|
|
3723
|
+
if (plain) {
|
|
3724
|
+
data.value = JSON.parse((0, import_tweetnacl_util.encodeUTF8)(plain));
|
|
3725
|
+
data.value_type = data.value_type.replace("encrypted:", "");
|
|
3726
|
+
}
|
|
3727
|
+
} catch {
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
return data;
|
|
3502
3731
|
}
|
|
3503
3732
|
/**
|
|
3504
3733
|
* Delete a key from persistent memory.
|
|
@@ -3967,31 +4196,40 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3967
4196
|
"Approximate message size (even with padding, bounded to power-of-2)"
|
|
3968
4197
|
],
|
|
3969
4198
|
relayCannotSee: [
|
|
3970
|
-
"Message content (E2E encrypted with
|
|
4199
|
+
"Message content (E2E encrypted \u2014 nacl.secretbox with ratchet-derived per-message keys)",
|
|
3971
4200
|
"Private keys (generated and stored client-side only)",
|
|
3972
|
-
"Memory values (encrypted with
|
|
3973
|
-
"
|
|
4201
|
+
"Memory values (encrypted CLIENT-SIDE with nacl.secretbox before relay storage)",
|
|
4202
|
+
"Past message keys (hash ratchet provides forward secrecy \u2014 old keys are deleted)",
|
|
3974
4203
|
...this.sealedSender ? ["Sender identity (sealed inside ciphertext)"] : []
|
|
3975
4204
|
],
|
|
3976
4205
|
protections: [
|
|
4206
|
+
"Hash ratchet forward secrecy \u2014 per-message key derivation, old keys deleted",
|
|
3977
4207
|
"X25519 key exchange + XSalsa20-Poly1305 authenticated encryption",
|
|
3978
|
-
"Ed25519 signatures on every message",
|
|
4208
|
+
"Ed25519 signatures on every message (envelope + ciphertext hash)",
|
|
3979
4209
|
"TOFU key pinning (MitM detection on key change)",
|
|
4210
|
+
"Client-side memory encryption (relay never sees plaintext values)",
|
|
4211
|
+
"Protocol version header (deterministic padding/sealing detection, no heuristics)",
|
|
4212
|
+
"Identity cache (reduced key lookups, 5-min TTL)",
|
|
4213
|
+
"Message deduplication (track seen message IDs)",
|
|
4214
|
+
"Request validation (fromCredentials validates key sizes and format)",
|
|
3980
4215
|
...this.paddingEnabled ? ["Message padding to power-of-2 boundary (traffic analysis resistance)"] : [],
|
|
3981
4216
|
...this.sealedSender ? ["Sealed sender (relay cannot see who sent a message)"] : [],
|
|
4217
|
+
...this.requireSignatures ? ["Strict signature enforcement (reject unsigned/invalid messages)"] : [],
|
|
3982
4218
|
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays)`] : [],
|
|
3983
4219
|
"Auto-retry with exponential backoff",
|
|
3984
|
-
"Offline message queue"
|
|
4220
|
+
"Offline message queue",
|
|
4221
|
+
"did:key interoperability (W3C standard DID format)"
|
|
3985
4222
|
],
|
|
3986
4223
|
gaps: [
|
|
3987
|
-
"No
|
|
3988
|
-
"No post-quantum protection \u2014 vulnerable to harvest-now-decrypt-later",
|
|
4224
|
+
"No DH ratchet yet \u2014 hash ratchet only (no post-compromise recovery, planned for v3)",
|
|
4225
|
+
"No post-quantum protection \u2014 vulnerable to harvest-now-decrypt-later (planned for v3)",
|
|
3989
4226
|
"Channel encryption is server-side (relay holds channel keys, NOT true E2E)",
|
|
3990
4227
|
"Metadata (who, when, thread structure) visible to relay operator",
|
|
3991
4228
|
"Single relay architecture (no onion routing, no mix network)",
|
|
3992
4229
|
"Ed25519 signatures are non-repudiable (no deniable messaging)",
|
|
3993
4230
|
"No async key agreement (no X3DH prekeys)",
|
|
3994
|
-
"Polling-based (no WebSocket real-time transport)"
|
|
4231
|
+
"Polling-based (no WebSocket real-time transport)",
|
|
4232
|
+
"Ratchet state is in-memory (lost on process restart \u2014 export credentials to persist)"
|
|
3995
4233
|
]
|
|
3996
4234
|
};
|
|
3997
4235
|
}
|
|
@@ -4017,6 +4255,9 @@ var Conversation = class {
|
|
|
4017
4255
|
replyTo: this._lastMessageId || void 0
|
|
4018
4256
|
});
|
|
4019
4257
|
this._lastMessageId = result.id;
|
|
4258
|
+
if (this._messageHistory.length >= 1e3) {
|
|
4259
|
+
this._messageHistory.splice(0, this._messageHistory.length - 999);
|
|
4260
|
+
}
|
|
4020
4261
|
this._messageHistory.push({
|
|
4021
4262
|
id: result.id,
|
|
4022
4263
|
from: this.agent.did,
|
|
@@ -4091,14 +4332,23 @@ var Conversation = class {
|
|
|
4091
4332
|
async waitForReply(timeoutMs = 3e4) {
|
|
4092
4333
|
return new Promise((resolve, reject) => {
|
|
4093
4334
|
let resolved = false;
|
|
4335
|
+
let pollTimer = null;
|
|
4336
|
+
const cleanup = () => {
|
|
4337
|
+
resolved = true;
|
|
4338
|
+
if (pollTimer) {
|
|
4339
|
+
clearTimeout(pollTimer);
|
|
4340
|
+
pollTimer = null;
|
|
4341
|
+
}
|
|
4342
|
+
};
|
|
4094
4343
|
const timeout = setTimeout(() => {
|
|
4095
4344
|
if (!resolved) {
|
|
4096
|
-
|
|
4345
|
+
cleanup();
|
|
4097
4346
|
reject(new Error(`No reply received within ${timeoutMs}ms`));
|
|
4098
4347
|
}
|
|
4099
4348
|
}, timeoutMs);
|
|
4100
4349
|
const check = async () => {
|
|
4101
|
-
|
|
4350
|
+
if (resolved) return;
|
|
4351
|
+
try {
|
|
4102
4352
|
const messages = await this.agent.receive({
|
|
4103
4353
|
from: this.peerDid,
|
|
4104
4354
|
threadId: this.threadId,
|
|
@@ -4106,9 +4356,12 @@ var Conversation = class {
|
|
|
4106
4356
|
limit: 1
|
|
4107
4357
|
});
|
|
4108
4358
|
if (messages.length > 0 && !resolved) {
|
|
4109
|
-
resolved = true;
|
|
4110
4359
|
clearTimeout(timeout);
|
|
4360
|
+
cleanup();
|
|
4111
4361
|
const msg = messages[0];
|
|
4362
|
+
if (this._messageHistory.length >= 1e3) {
|
|
4363
|
+
this._messageHistory.splice(0, this._messageHistory.length - 999);
|
|
4364
|
+
}
|
|
4112
4365
|
this._messageHistory.push({
|
|
4113
4366
|
id: msg.id,
|
|
4114
4367
|
from: msg.from,
|
|
@@ -4123,16 +4376,19 @@ var Conversation = class {
|
|
|
4123
4376
|
resolve(msg);
|
|
4124
4377
|
return;
|
|
4125
4378
|
}
|
|
4126
|
-
|
|
4379
|
+
} catch (err) {
|
|
4380
|
+
if (!resolved) {
|
|
4381
|
+
clearTimeout(timeout);
|
|
4382
|
+
cleanup();
|
|
4383
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
4384
|
+
return;
|
|
4385
|
+
}
|
|
4127
4386
|
}
|
|
4128
|
-
};
|
|
4129
|
-
check().catch((err) => {
|
|
4130
4387
|
if (!resolved) {
|
|
4131
|
-
|
|
4132
|
-
clearTimeout(timeout);
|
|
4133
|
-
reject(err);
|
|
4388
|
+
pollTimer = setTimeout(check, 1500);
|
|
4134
4389
|
}
|
|
4135
|
-
}
|
|
4390
|
+
};
|
|
4391
|
+
check();
|
|
4136
4392
|
});
|
|
4137
4393
|
}
|
|
4138
4394
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidly/agent-sdk",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "E2E encrypted agent-to-agent communication SDK — padding, sealed sender,
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "E2E encrypted agent-to-agent communication SDK — forward secrecy, padding, sealed sender, client-side memory encryption",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|