@voidly/agent-sdk 1.9.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 +68 -6
- package/dist/index.d.ts +68 -6
- package/dist/index.js +516 -53
- package/dist/index.mjs +516 -53
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2346,11 +2346,107 @@ 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
|
+
}
|
|
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
|
+
}
|
|
2349
2440
|
var VoidlyAgent = class _VoidlyAgent {
|
|
2350
2441
|
constructor(identity, config) {
|
|
2351
2442
|
this._pinnedDids = /* @__PURE__ */ new Set();
|
|
2352
2443
|
this._listeners = /* @__PURE__ */ new Set();
|
|
2353
2444
|
this._conversations = /* @__PURE__ */ new Map();
|
|
2445
|
+
this._offlineQueue = [];
|
|
2446
|
+
this._ratchetStates = /* @__PURE__ */ new Map();
|
|
2447
|
+
this._identityCache = /* @__PURE__ */ new Map();
|
|
2448
|
+
this._seenMessageIds = /* @__PURE__ */ new Set();
|
|
2449
|
+
this._decryptFailCount = 0;
|
|
2354
2450
|
this.did = identity.did;
|
|
2355
2451
|
this.apiKey = identity.apiKey;
|
|
2356
2452
|
this.signingKeyPair = identity.signingKeyPair;
|
|
@@ -2358,6 +2454,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2358
2454
|
this.baseUrl = config?.baseUrl || "https://api.voidly.ai";
|
|
2359
2455
|
this.autoPin = config?.autoPin !== false;
|
|
2360
2456
|
this.defaultRetries = config?.retries ?? 3;
|
|
2457
|
+
this.fallbackRelays = config?.fallbackRelays || [];
|
|
2458
|
+
this.paddingEnabled = config?.padding !== false;
|
|
2459
|
+
this.sealedSender = config?.sealedSender || false;
|
|
2460
|
+
this.requireSignatures = config?.requireSignatures || false;
|
|
2461
|
+
this.timeout = config?.timeout ?? 3e4;
|
|
2361
2462
|
}
|
|
2362
2463
|
// ─── Factory Methods ────────────────────────────────────────────────────────
|
|
2363
2464
|
/**
|
|
@@ -2396,8 +2497,29 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2396
2497
|
* Use this to resume an agent across sessions.
|
|
2397
2498
|
*/
|
|
2398
2499
|
static fromCredentials(creds, config) {
|
|
2399
|
-
|
|
2400
|
-
|
|
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
|
+
}
|
|
2401
2523
|
return new _VoidlyAgent({
|
|
2402
2524
|
did: creds.did,
|
|
2403
2525
|
apiKey: creds.apiKey,
|
|
@@ -2422,18 +2544,43 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2422
2544
|
encryptionPublicKey: (0, import_tweetnacl_util.encodeBase64)(this.encryptionKeyPair.publicKey)
|
|
2423
2545
|
};
|
|
2424
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
|
+
}
|
|
2425
2566
|
// ─── Messaging ──────────────────────────────────────────────────────────────
|
|
2426
2567
|
/**
|
|
2427
|
-
* Send an E2E encrypted message with
|
|
2568
|
+
* Send an E2E encrypted message with hardened security.
|
|
2428
2569
|
* Encryption happens locally — the relay NEVER sees plaintext or private keys.
|
|
2429
2570
|
*
|
|
2430
|
-
*
|
|
2571
|
+
* Security features:
|
|
2572
|
+
* - **Message padding** — ciphertext padded to power-of-2 boundary (traffic analysis resistance)
|
|
2573
|
+
* - **Hash ratchet** — per-conversation forward secrecy (compromise key[n] can't derive key[n-1])
|
|
2574
|
+
* - **Sealed sender** — optionally hide sender DID from relay metadata
|
|
2431
2575
|
* - **Auto-retry** with exponential backoff on transient failures
|
|
2432
|
-
* - **
|
|
2433
|
-
* - **
|
|
2576
|
+
* - **Multi-relay fallback** — try backup relays if primary is down
|
|
2577
|
+
* - **Offline queue** — queue messages if all relays fail
|
|
2578
|
+
* - **Transparent TOFU** — auto-pin recipient keys on first contact
|
|
2434
2579
|
*/
|
|
2435
2580
|
async send(recipientDid, message, options = {}) {
|
|
2436
2581
|
const maxRetries = options.retries ?? this.defaultRetries;
|
|
2582
|
+
const usePadding = !options.noPadding && this.paddingEnabled;
|
|
2583
|
+
const useSealed = options.sealedSender ?? this.sealedSender;
|
|
2437
2584
|
const profile = await this.getIdentity(recipientDid);
|
|
2438
2585
|
if (!profile) {
|
|
2439
2586
|
throw new Error(`Recipient ${recipientDid} not found`);
|
|
@@ -2442,9 +2589,43 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2442
2589
|
await this._autoPinKeys(recipientDid);
|
|
2443
2590
|
}
|
|
2444
2591
|
const recipientPubKey = (0, import_tweetnacl_util.decodeBase64)(profile.encryption_public_key);
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2592
|
+
let plaintext = message;
|
|
2593
|
+
if (useSealed) {
|
|
2594
|
+
plaintext = sealEnvelope(this.did, message);
|
|
2595
|
+
}
|
|
2596
|
+
let contentBytes;
|
|
2597
|
+
if (usePadding) {
|
|
2598
|
+
contentBytes = padMessage((0, import_tweetnacl_util.decodeUTF8)(plaintext));
|
|
2599
|
+
} else {
|
|
2600
|
+
contentBytes = (0, import_tweetnacl_util.decodeUTF8)(plaintext);
|
|
2601
|
+
}
|
|
2602
|
+
const pairId = `${this.did}:${recipientDid}`;
|
|
2603
|
+
let state = this._ratchetStates.get(pairId);
|
|
2604
|
+
if (!state) {
|
|
2605
|
+
const sharedSecret = import_tweetnacl.default.box.before(recipientPubKey, this.encryptionKeyPair.secretKey);
|
|
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);
|
|
2615
|
+
}
|
|
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);
|
|
2448
2629
|
if (!ciphertext) {
|
|
2449
2630
|
throw new Error("Encryption failed");
|
|
2450
2631
|
}
|
|
@@ -2453,7 +2634,8 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2453
2634
|
to: recipientDid,
|
|
2454
2635
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2455
2636
|
nonce: (0, import_tweetnacl_util.encodeBase64)(nonce),
|
|
2456
|
-
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
|
|
2457
2639
|
});
|
|
2458
2640
|
const signature = import_tweetnacl.default.sign.detached((0, import_tweetnacl_util.decodeUTF8)(envelopeData), this.signingKeyPair.secretKey);
|
|
2459
2641
|
const payload = {
|
|
@@ -2468,24 +2650,42 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2468
2650
|
reply_to: options.replyTo,
|
|
2469
2651
|
ttl: options.ttl
|
|
2470
2652
|
};
|
|
2471
|
-
const
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2653
|
+
const relays = [this.baseUrl, ...this.fallbackRelays];
|
|
2654
|
+
let lastError = null;
|
|
2655
|
+
for (const relay of relays) {
|
|
2656
|
+
try {
|
|
2657
|
+
const raw = await this._fetchWithRetry(
|
|
2658
|
+
`${relay}/v1/agent/send/encrypted`,
|
|
2659
|
+
{
|
|
2660
|
+
method: "POST",
|
|
2661
|
+
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
2662
|
+
body: JSON.stringify(payload)
|
|
2663
|
+
},
|
|
2664
|
+
{ maxRetries, baseDelay: 500, maxDelay: 1e4 }
|
|
2665
|
+
);
|
|
2666
|
+
return {
|
|
2667
|
+
id: raw.id,
|
|
2668
|
+
from: raw.from,
|
|
2669
|
+
to: raw.to,
|
|
2670
|
+
timestamp: raw.timestamp,
|
|
2671
|
+
expiresAt: raw.expires_at || raw.expiresAt,
|
|
2672
|
+
encrypted: raw.encrypted,
|
|
2673
|
+
clientSide: raw.client_side || raw.clientSide
|
|
2674
|
+
};
|
|
2675
|
+
} catch (err) {
|
|
2676
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2677
|
+
if (lastError.message.includes("(4")) break;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
if (lastError && !lastError.message.includes("(4")) {
|
|
2681
|
+
this._offlineQueue.push({
|
|
2682
|
+
recipientDid,
|
|
2683
|
+
message,
|
|
2684
|
+
options,
|
|
2685
|
+
timestamp: Date.now()
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
throw lastError || new Error("Send failed");
|
|
2489
2689
|
}
|
|
2490
2690
|
/**
|
|
2491
2691
|
* Receive and decrypt messages. Decryption happens locally.
|
|
@@ -2511,17 +2711,105 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2511
2711
|
const decrypted = [];
|
|
2512
2712
|
for (const msg of data.messages) {
|
|
2513
2713
|
try {
|
|
2714
|
+
if (this._seenMessageIds.has(msg.id)) continue;
|
|
2514
2715
|
const senderEncPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_encryption_key);
|
|
2515
2716
|
const ciphertext = (0, import_tweetnacl_util.decodeBase64)(msg.ciphertext);
|
|
2516
2717
|
const nonce = (0, import_tweetnacl_util.decodeBase64)(msg.nonce);
|
|
2517
|
-
|
|
2518
|
-
|
|
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
|
+
}
|
|
2781
|
+
let plaintextBytes = rawPlaintext;
|
|
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) {
|
|
2793
|
+
const unpadded = unpadMessage(rawPlaintext);
|
|
2794
|
+
if (unpadded.length < rawPlaintext.length) {
|
|
2795
|
+
plaintextBytes = unpadded;
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
let content = (0, import_tweetnacl_util.encodeUTF8)(plaintextBytes);
|
|
2799
|
+
let senderDid = msg.from;
|
|
2800
|
+
if (wasSealed || !proto) {
|
|
2801
|
+
const unsealed = unsealEnvelope(content);
|
|
2802
|
+
if (unsealed) {
|
|
2803
|
+
content = unsealed.msg;
|
|
2804
|
+
senderDid = unsealed.from;
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2519
2807
|
let signatureValid = false;
|
|
2520
2808
|
try {
|
|
2521
2809
|
const senderSignPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_signing_key);
|
|
2522
2810
|
const signatureBytes = (0, import_tweetnacl_util.decodeBase64)(msg.signature);
|
|
2523
2811
|
const envelopeStr = msg.envelope || JSON.stringify({
|
|
2524
|
-
from:
|
|
2812
|
+
from: senderDid,
|
|
2525
2813
|
to: msg.to,
|
|
2526
2814
|
timestamp: msg.timestamp,
|
|
2527
2815
|
nonce: msg.nonce,
|
|
@@ -2535,11 +2823,20 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2535
2823
|
} catch {
|
|
2536
2824
|
signatureValid = false;
|
|
2537
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
|
+
}
|
|
2538
2835
|
decrypted.push({
|
|
2539
2836
|
id: msg.id,
|
|
2540
|
-
from:
|
|
2837
|
+
from: senderDid,
|
|
2541
2838
|
to: msg.to,
|
|
2542
|
-
content
|
|
2839
|
+
content,
|
|
2543
2840
|
contentType: msg.content_type,
|
|
2544
2841
|
messageType: msg.message_type || "text",
|
|
2545
2842
|
threadId: msg.thread_id,
|
|
@@ -2549,6 +2846,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2549
2846
|
expiresAt: msg.expires_at
|
|
2550
2847
|
});
|
|
2551
2848
|
} catch {
|
|
2849
|
+
this._decryptFailCount++;
|
|
2552
2850
|
}
|
|
2553
2851
|
}
|
|
2554
2852
|
return decrypted;
|
|
@@ -2599,9 +2897,19 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2599
2897
|
* Look up an agent's public profile and keys.
|
|
2600
2898
|
*/
|
|
2601
2899
|
async getIdentity(did) {
|
|
2900
|
+
const cached = this._identityCache.get(did);
|
|
2901
|
+
if (cached && Date.now() - cached.cachedAt < 3e5) {
|
|
2902
|
+
return cached.profile;
|
|
2903
|
+
}
|
|
2602
2904
|
const res = await fetch(`${this.baseUrl}/v1/agent/identity/${did}`);
|
|
2603
2905
|
if (!res.ok) return null;
|
|
2604
|
-
|
|
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;
|
|
2605
2913
|
}
|
|
2606
2914
|
/**
|
|
2607
2915
|
* Search for agents by name or capability.
|
|
@@ -2661,10 +2969,14 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2661
2969
|
* Delete a webhook.
|
|
2662
2970
|
*/
|
|
2663
2971
|
async deleteWebhook(webhookId) {
|
|
2664
|
-
await fetch(`${this.baseUrl}/v1/agent/webhooks/${webhookId}`, {
|
|
2972
|
+
const res = await fetch(`${this.baseUrl}/v1/agent/webhooks/${webhookId}`, {
|
|
2665
2973
|
method: "DELETE",
|
|
2666
2974
|
headers: { "X-Agent-Key": this.apiKey }
|
|
2667
2975
|
});
|
|
2976
|
+
if (!res.ok) {
|
|
2977
|
+
const err = await res.json().catch(() => ({}));
|
|
2978
|
+
throw new Error(`Webhook delete failed: ${err.error || res.statusText}`);
|
|
2979
|
+
}
|
|
2668
2980
|
}
|
|
2669
2981
|
/**
|
|
2670
2982
|
* Verify a webhook payload signature (for use in your webhook handler).
|
|
@@ -3282,13 +3594,21 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3282
3594
|
* The relay finds matching agents and creates individual tasks for each.
|
|
3283
3595
|
*/
|
|
3284
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);
|
|
3285
3604
|
const res = await fetch(`${this.baseUrl}/v1/agent/tasks/broadcast`, {
|
|
3286
3605
|
method: "POST",
|
|
3287
3606
|
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
3288
3607
|
body: JSON.stringify({
|
|
3289
3608
|
capability: options.capability,
|
|
3290
|
-
encrypted_input: (0, import_tweetnacl_util.encodeBase64)(
|
|
3291
|
-
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),
|
|
3292
3612
|
priority: options.priority,
|
|
3293
3613
|
max_agents: options.maxAgents,
|
|
3294
3614
|
min_trust_level: options.minTrustLevel,
|
|
@@ -3358,16 +3678,30 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3358
3678
|
// ─── Memory Store ──────────────────────────────────────────────────────────
|
|
3359
3679
|
/**
|
|
3360
3680
|
* Store an encrypted key-value pair in persistent memory.
|
|
3361
|
-
* 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.
|
|
3362
3683
|
*/
|
|
3363
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);
|
|
3364
3697
|
const res = await fetch(`${this.baseUrl}/v1/agent/memory/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`, {
|
|
3365
3698
|
method: "PUT",
|
|
3366
3699
|
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
3367
3700
|
body: JSON.stringify({
|
|
3368
|
-
value,
|
|
3369
|
-
value_type: options?.valueType || (typeof value === "object" ? "json" : typeof value)
|
|
3370
|
-
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)
|
|
3371
3705
|
})
|
|
3372
3706
|
});
|
|
3373
3707
|
if (!res.ok) throw new Error(`Memory set failed: ${res.status} ${await res.text()}`);
|
|
@@ -3375,7 +3709,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3375
3709
|
}
|
|
3376
3710
|
/**
|
|
3377
3711
|
* Retrieve a value from persistent memory.
|
|
3378
|
-
* Decrypted
|
|
3712
|
+
* Decrypted CLIENT-SIDE — relay never sees plaintext.
|
|
3379
3713
|
*/
|
|
3380
3714
|
async memoryGet(namespace, key) {
|
|
3381
3715
|
const res = await fetch(`${this.baseUrl}/v1/agent/memory/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`, {
|
|
@@ -3383,7 +3717,28 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3383
3717
|
});
|
|
3384
3718
|
if (res.status === 404) return null;
|
|
3385
3719
|
if (!res.ok) throw new Error(`Memory get failed: ${res.status} ${await res.text()}`);
|
|
3386
|
-
|
|
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;
|
|
3387
3742
|
}
|
|
3388
3743
|
/**
|
|
3389
3744
|
* Delete a key from persistent memory.
|
|
@@ -3799,6 +4154,96 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3799
4154
|
}
|
|
3800
4155
|
throw lastError || new Error("Send failed after retries");
|
|
3801
4156
|
}
|
|
4157
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4158
|
+
// OFFLINE QUEUE — Resilience against relay downtime
|
|
4159
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4160
|
+
/**
|
|
4161
|
+
* Drain the offline message queue — retry sending queued messages.
|
|
4162
|
+
* Call this when connectivity is restored.
|
|
4163
|
+
* Returns: number of messages successfully sent.
|
|
4164
|
+
*/
|
|
4165
|
+
async drainQueue() {
|
|
4166
|
+
let sent = 0;
|
|
4167
|
+
let failed = 0;
|
|
4168
|
+
const remaining = [];
|
|
4169
|
+
for (const item of this._offlineQueue) {
|
|
4170
|
+
if (Date.now() - item.timestamp > 864e5) {
|
|
4171
|
+
failed++;
|
|
4172
|
+
continue;
|
|
4173
|
+
}
|
|
4174
|
+
try {
|
|
4175
|
+
await this.send(item.recipientDid, item.message, item.options);
|
|
4176
|
+
sent++;
|
|
4177
|
+
} catch {
|
|
4178
|
+
remaining.push(item);
|
|
4179
|
+
failed++;
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
this._offlineQueue = remaining;
|
|
4183
|
+
return { sent, failed, remaining: remaining.length };
|
|
4184
|
+
}
|
|
4185
|
+
/** Number of messages waiting in the offline queue */
|
|
4186
|
+
get queueLength() {
|
|
4187
|
+
return this._offlineQueue.length;
|
|
4188
|
+
}
|
|
4189
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4190
|
+
// SECURITY REPORT — Transparent threat model
|
|
4191
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4192
|
+
/**
|
|
4193
|
+
* Returns what the relay can and cannot see about this agent.
|
|
4194
|
+
* Call this to understand your threat model. Total transparency.
|
|
4195
|
+
*/
|
|
4196
|
+
threatModel() {
|
|
4197
|
+
return {
|
|
4198
|
+
relayCanSee: [
|
|
4199
|
+
"Your DID (public identifier)",
|
|
4200
|
+
"Who you message (recipient DIDs)",
|
|
4201
|
+
"When you message (timestamps)",
|
|
4202
|
+
"Message types (text, task-request, etc.)",
|
|
4203
|
+
"Thread structure (which messages are replies)",
|
|
4204
|
+
"Channel membership",
|
|
4205
|
+
"Capability registrations",
|
|
4206
|
+
"Online/offline status",
|
|
4207
|
+
"Approximate message size (even with padding, bounded to power-of-2)"
|
|
4208
|
+
],
|
|
4209
|
+
relayCannotSee: [
|
|
4210
|
+
"Message content (E2E encrypted \u2014 nacl.secretbox with ratchet-derived per-message keys)",
|
|
4211
|
+
"Private keys (generated and stored client-side only)",
|
|
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)",
|
|
4214
|
+
...this.sealedSender ? ["Sender identity (sealed inside ciphertext)"] : []
|
|
4215
|
+
],
|
|
4216
|
+
protections: [
|
|
4217
|
+
"Hash ratchet forward secrecy \u2014 per-message key derivation, old keys deleted",
|
|
4218
|
+
"X25519 key exchange + XSalsa20-Poly1305 authenticated encryption",
|
|
4219
|
+
"Ed25519 signatures on every message (envelope + ciphertext hash)",
|
|
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)",
|
|
4226
|
+
...this.paddingEnabled ? ["Message padding to power-of-2 boundary (traffic analysis resistance)"] : [],
|
|
4227
|
+
...this.sealedSender ? ["Sealed sender (relay cannot see who sent a message)"] : [],
|
|
4228
|
+
...this.requireSignatures ? ["Strict signature enforcement (reject unsigned/invalid messages)"] : [],
|
|
4229
|
+
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays)`] : [],
|
|
4230
|
+
"Auto-retry with exponential backoff",
|
|
4231
|
+
"Offline message queue",
|
|
4232
|
+
"did:key interoperability (W3C standard DID format)"
|
|
4233
|
+
],
|
|
4234
|
+
gaps: [
|
|
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)",
|
|
4237
|
+
"Channel encryption is server-side (relay holds channel keys, NOT true E2E)",
|
|
4238
|
+
"Metadata (who, when, thread structure) visible to relay operator",
|
|
4239
|
+
"Single relay architecture (no onion routing, no mix network)",
|
|
4240
|
+
"Ed25519 signatures are non-repudiable (no deniable messaging)",
|
|
4241
|
+
"No async key agreement (no X3DH prekeys)",
|
|
4242
|
+
"Polling-based (no WebSocket real-time transport)",
|
|
4243
|
+
"Ratchet state is in-memory (lost on process restart \u2014 export credentials to persist)"
|
|
4244
|
+
]
|
|
4245
|
+
};
|
|
4246
|
+
}
|
|
3802
4247
|
};
|
|
3803
4248
|
var Conversation = class {
|
|
3804
4249
|
/** @internal */
|
|
@@ -3821,6 +4266,9 @@ var Conversation = class {
|
|
|
3821
4266
|
replyTo: this._lastMessageId || void 0
|
|
3822
4267
|
});
|
|
3823
4268
|
this._lastMessageId = result.id;
|
|
4269
|
+
if (this._messageHistory.length >= 1e3) {
|
|
4270
|
+
this._messageHistory.splice(0, this._messageHistory.length - 999);
|
|
4271
|
+
}
|
|
3824
4272
|
this._messageHistory.push({
|
|
3825
4273
|
id: result.id,
|
|
3826
4274
|
from: this.agent.did,
|
|
@@ -3895,14 +4343,23 @@ var Conversation = class {
|
|
|
3895
4343
|
async waitForReply(timeoutMs = 3e4) {
|
|
3896
4344
|
return new Promise((resolve, reject) => {
|
|
3897
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
|
+
};
|
|
3898
4354
|
const timeout = setTimeout(() => {
|
|
3899
4355
|
if (!resolved) {
|
|
3900
|
-
|
|
4356
|
+
cleanup();
|
|
3901
4357
|
reject(new Error(`No reply received within ${timeoutMs}ms`));
|
|
3902
4358
|
}
|
|
3903
4359
|
}, timeoutMs);
|
|
3904
4360
|
const check = async () => {
|
|
3905
|
-
|
|
4361
|
+
if (resolved) return;
|
|
4362
|
+
try {
|
|
3906
4363
|
const messages = await this.agent.receive({
|
|
3907
4364
|
from: this.peerDid,
|
|
3908
4365
|
threadId: this.threadId,
|
|
@@ -3910,9 +4367,12 @@ var Conversation = class {
|
|
|
3910
4367
|
limit: 1
|
|
3911
4368
|
});
|
|
3912
4369
|
if (messages.length > 0 && !resolved) {
|
|
3913
|
-
resolved = true;
|
|
3914
4370
|
clearTimeout(timeout);
|
|
4371
|
+
cleanup();
|
|
3915
4372
|
const msg = messages[0];
|
|
4373
|
+
if (this._messageHistory.length >= 1e3) {
|
|
4374
|
+
this._messageHistory.splice(0, this._messageHistory.length - 999);
|
|
4375
|
+
}
|
|
3916
4376
|
this._messageHistory.push({
|
|
3917
4377
|
id: msg.id,
|
|
3918
4378
|
from: msg.from,
|
|
@@ -3927,16 +4387,19 @@ var Conversation = class {
|
|
|
3927
4387
|
resolve(msg);
|
|
3928
4388
|
return;
|
|
3929
4389
|
}
|
|
3930
|
-
|
|
4390
|
+
} catch (err) {
|
|
4391
|
+
if (!resolved) {
|
|
4392
|
+
clearTimeout(timeout);
|
|
4393
|
+
cleanup();
|
|
4394
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
3931
4397
|
}
|
|
3932
|
-
};
|
|
3933
|
-
check().catch((err) => {
|
|
3934
4398
|
if (!resolved) {
|
|
3935
|
-
|
|
3936
|
-
clearTimeout(timeout);
|
|
3937
|
-
reject(err);
|
|
4399
|
+
pollTimer = setTimeout(check, 1500);
|
|
3938
4400
|
}
|
|
3939
|
-
}
|
|
4401
|
+
};
|
|
4402
|
+
check();
|
|
3940
4403
|
});
|
|
3941
4404
|
}
|
|
3942
4405
|
/**
|