@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.mjs
CHANGED
|
@@ -2335,11 +2335,107 @@ async function sha256(data) {
|
|
|
2335
2335
|
const { createHash } = await import("crypto");
|
|
2336
2336
|
return createHash("sha256").update(data).digest("hex");
|
|
2337
2337
|
}
|
|
2338
|
+
function padMessage(content) {
|
|
2339
|
+
const contentLen = content.length;
|
|
2340
|
+
const totalLen = Math.max(256, nextPowerOf2(contentLen + 2));
|
|
2341
|
+
const padded = new Uint8Array(totalLen);
|
|
2342
|
+
padded[0] = contentLen >> 8 & 255;
|
|
2343
|
+
padded[1] = contentLen & 255;
|
|
2344
|
+
padded.set(content, 2);
|
|
2345
|
+
const randomPad = import_tweetnacl.default.randomBytes(totalLen - contentLen - 2);
|
|
2346
|
+
padded.set(randomPad, contentLen + 2);
|
|
2347
|
+
return padded;
|
|
2348
|
+
}
|
|
2349
|
+
function unpadMessage(padded) {
|
|
2350
|
+
if (padded.length < 2) return padded;
|
|
2351
|
+
const contentLen = padded[0] << 8 | padded[1];
|
|
2352
|
+
if (contentLen + 2 > padded.length) return padded;
|
|
2353
|
+
return padded.slice(2, 2 + contentLen);
|
|
2354
|
+
}
|
|
2355
|
+
function nextPowerOf2(n) {
|
|
2356
|
+
let p = 1;
|
|
2357
|
+
while (p < n) p <<= 1;
|
|
2358
|
+
return p;
|
|
2359
|
+
}
|
|
2360
|
+
async function ratchetStep(chainKey) {
|
|
2361
|
+
const encoder = new TextEncoder();
|
|
2362
|
+
if (typeof globalThis.crypto?.subtle !== "undefined") {
|
|
2363
|
+
const ckInput = new Uint8Array([...chainKey, 1]);
|
|
2364
|
+
const mkInput = new Uint8Array([...chainKey, 2]);
|
|
2365
|
+
const nextChainKey2 = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", ckInput));
|
|
2366
|
+
const messageKey2 = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", mkInput));
|
|
2367
|
+
return { nextChainKey: nextChainKey2, messageKey: messageKey2 };
|
|
2368
|
+
}
|
|
2369
|
+
const { createHash } = await import("crypto");
|
|
2370
|
+
const nextChainKey = new Uint8Array(
|
|
2371
|
+
createHash("sha256").update(Buffer.from([...chainKey, 1])).digest()
|
|
2372
|
+
);
|
|
2373
|
+
const messageKey = new Uint8Array(
|
|
2374
|
+
createHash("sha256").update(Buffer.from([...chainKey, 2])).digest()
|
|
2375
|
+
);
|
|
2376
|
+
return { nextChainKey, messageKey };
|
|
2377
|
+
}
|
|
2378
|
+
function sealEnvelope(senderDid, plaintext) {
|
|
2379
|
+
return JSON.stringify({
|
|
2380
|
+
v: 2,
|
|
2381
|
+
from: senderDid,
|
|
2382
|
+
msg: plaintext,
|
|
2383
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
function unsealEnvelope(plaintext) {
|
|
2387
|
+
try {
|
|
2388
|
+
const parsed = JSON.parse(plaintext);
|
|
2389
|
+
if (parsed.v === 2 && parsed.from && parsed.msg) {
|
|
2390
|
+
return { from: parsed.from, msg: parsed.msg, ts: parsed.ts };
|
|
2391
|
+
}
|
|
2392
|
+
return null;
|
|
2393
|
+
} catch {
|
|
2394
|
+
return null;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
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
|
+
}
|
|
2338
2429
|
var VoidlyAgent = class _VoidlyAgent {
|
|
2339
2430
|
constructor(identity, config) {
|
|
2340
2431
|
this._pinnedDids = /* @__PURE__ */ new Set();
|
|
2341
2432
|
this._listeners = /* @__PURE__ */ new Set();
|
|
2342
2433
|
this._conversations = /* @__PURE__ */ new Map();
|
|
2434
|
+
this._offlineQueue = [];
|
|
2435
|
+
this._ratchetStates = /* @__PURE__ */ new Map();
|
|
2436
|
+
this._identityCache = /* @__PURE__ */ new Map();
|
|
2437
|
+
this._seenMessageIds = /* @__PURE__ */ new Set();
|
|
2438
|
+
this._decryptFailCount = 0;
|
|
2343
2439
|
this.did = identity.did;
|
|
2344
2440
|
this.apiKey = identity.apiKey;
|
|
2345
2441
|
this.signingKeyPair = identity.signingKeyPair;
|
|
@@ -2347,6 +2443,11 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2347
2443
|
this.baseUrl = config?.baseUrl || "https://api.voidly.ai";
|
|
2348
2444
|
this.autoPin = config?.autoPin !== false;
|
|
2349
2445
|
this.defaultRetries = config?.retries ?? 3;
|
|
2446
|
+
this.fallbackRelays = config?.fallbackRelays || [];
|
|
2447
|
+
this.paddingEnabled = config?.padding !== false;
|
|
2448
|
+
this.sealedSender = config?.sealedSender || false;
|
|
2449
|
+
this.requireSignatures = config?.requireSignatures || false;
|
|
2450
|
+
this.timeout = config?.timeout ?? 3e4;
|
|
2350
2451
|
}
|
|
2351
2452
|
// ─── Factory Methods ────────────────────────────────────────────────────────
|
|
2352
2453
|
/**
|
|
@@ -2385,8 +2486,29 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2385
2486
|
* Use this to resume an agent across sessions.
|
|
2386
2487
|
*/
|
|
2387
2488
|
static fromCredentials(creds, config) {
|
|
2388
|
-
|
|
2389
|
-
|
|
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
|
+
}
|
|
2390
2512
|
return new _VoidlyAgent({
|
|
2391
2513
|
did: creds.did,
|
|
2392
2514
|
apiKey: creds.apiKey,
|
|
@@ -2411,18 +2533,43 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2411
2533
|
encryptionPublicKey: (0, import_tweetnacl_util.encodeBase64)(this.encryptionKeyPair.publicKey)
|
|
2412
2534
|
};
|
|
2413
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
|
+
}
|
|
2414
2555
|
// ─── Messaging ──────────────────────────────────────────────────────────────
|
|
2415
2556
|
/**
|
|
2416
|
-
* Send an E2E encrypted message with
|
|
2557
|
+
* Send an E2E encrypted message with hardened security.
|
|
2417
2558
|
* Encryption happens locally — the relay NEVER sees plaintext or private keys.
|
|
2418
2559
|
*
|
|
2419
|
-
*
|
|
2560
|
+
* Security features:
|
|
2561
|
+
* - **Message padding** — ciphertext padded to power-of-2 boundary (traffic analysis resistance)
|
|
2562
|
+
* - **Hash ratchet** — per-conversation forward secrecy (compromise key[n] can't derive key[n-1])
|
|
2563
|
+
* - **Sealed sender** — optionally hide sender DID from relay metadata
|
|
2420
2564
|
* - **Auto-retry** with exponential backoff on transient failures
|
|
2421
|
-
* - **
|
|
2422
|
-
* - **
|
|
2565
|
+
* - **Multi-relay fallback** — try backup relays if primary is down
|
|
2566
|
+
* - **Offline queue** — queue messages if all relays fail
|
|
2567
|
+
* - **Transparent TOFU** — auto-pin recipient keys on first contact
|
|
2423
2568
|
*/
|
|
2424
2569
|
async send(recipientDid, message, options = {}) {
|
|
2425
2570
|
const maxRetries = options.retries ?? this.defaultRetries;
|
|
2571
|
+
const usePadding = !options.noPadding && this.paddingEnabled;
|
|
2572
|
+
const useSealed = options.sealedSender ?? this.sealedSender;
|
|
2426
2573
|
const profile = await this.getIdentity(recipientDid);
|
|
2427
2574
|
if (!profile) {
|
|
2428
2575
|
throw new Error(`Recipient ${recipientDid} not found`);
|
|
@@ -2431,9 +2578,43 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2431
2578
|
await this._autoPinKeys(recipientDid);
|
|
2432
2579
|
}
|
|
2433
2580
|
const recipientPubKey = (0, import_tweetnacl_util.decodeBase64)(profile.encryption_public_key);
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2581
|
+
let plaintext = message;
|
|
2582
|
+
if (useSealed) {
|
|
2583
|
+
plaintext = sealEnvelope(this.did, message);
|
|
2584
|
+
}
|
|
2585
|
+
let contentBytes;
|
|
2586
|
+
if (usePadding) {
|
|
2587
|
+
contentBytes = padMessage((0, import_tweetnacl_util.decodeUTF8)(plaintext));
|
|
2588
|
+
} else {
|
|
2589
|
+
contentBytes = (0, import_tweetnacl_util.decodeUTF8)(plaintext);
|
|
2590
|
+
}
|
|
2591
|
+
const pairId = `${this.did}:${recipientDid}`;
|
|
2592
|
+
let state = this._ratchetStates.get(pairId);
|
|
2593
|
+
if (!state) {
|
|
2594
|
+
const sharedSecret = import_tweetnacl.default.box.before(recipientPubKey, this.encryptionKeyPair.secretKey);
|
|
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);
|
|
2604
|
+
}
|
|
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);
|
|
2437
2618
|
if (!ciphertext) {
|
|
2438
2619
|
throw new Error("Encryption failed");
|
|
2439
2620
|
}
|
|
@@ -2442,7 +2623,8 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2442
2623
|
to: recipientDid,
|
|
2443
2624
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2444
2625
|
nonce: (0, import_tweetnacl_util.encodeBase64)(nonce),
|
|
2445
|
-
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
|
|
2446
2628
|
});
|
|
2447
2629
|
const signature = import_tweetnacl.default.sign.detached((0, import_tweetnacl_util.decodeUTF8)(envelopeData), this.signingKeyPair.secretKey);
|
|
2448
2630
|
const payload = {
|
|
@@ -2457,24 +2639,42 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2457
2639
|
reply_to: options.replyTo,
|
|
2458
2640
|
ttl: options.ttl
|
|
2459
2641
|
};
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2642
|
+
const relays = [this.baseUrl, ...this.fallbackRelays];
|
|
2643
|
+
let lastError = null;
|
|
2644
|
+
for (const relay of relays) {
|
|
2645
|
+
try {
|
|
2646
|
+
const raw = await this._fetchWithRetry(
|
|
2647
|
+
`${relay}/v1/agent/send/encrypted`,
|
|
2648
|
+
{
|
|
2649
|
+
method: "POST",
|
|
2650
|
+
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
2651
|
+
body: JSON.stringify(payload)
|
|
2652
|
+
},
|
|
2653
|
+
{ maxRetries, baseDelay: 500, maxDelay: 1e4 }
|
|
2654
|
+
);
|
|
2655
|
+
return {
|
|
2656
|
+
id: raw.id,
|
|
2657
|
+
from: raw.from,
|
|
2658
|
+
to: raw.to,
|
|
2659
|
+
timestamp: raw.timestamp,
|
|
2660
|
+
expiresAt: raw.expires_at || raw.expiresAt,
|
|
2661
|
+
encrypted: raw.encrypted,
|
|
2662
|
+
clientSide: raw.client_side || raw.clientSide
|
|
2663
|
+
};
|
|
2664
|
+
} catch (err) {
|
|
2665
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2666
|
+
if (lastError.message.includes("(4")) break;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
if (lastError && !lastError.message.includes("(4")) {
|
|
2670
|
+
this._offlineQueue.push({
|
|
2671
|
+
recipientDid,
|
|
2672
|
+
message,
|
|
2673
|
+
options,
|
|
2674
|
+
timestamp: Date.now()
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2677
|
+
throw lastError || new Error("Send failed");
|
|
2478
2678
|
}
|
|
2479
2679
|
/**
|
|
2480
2680
|
* Receive and decrypt messages. Decryption happens locally.
|
|
@@ -2500,17 +2700,105 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2500
2700
|
const decrypted = [];
|
|
2501
2701
|
for (const msg of data.messages) {
|
|
2502
2702
|
try {
|
|
2703
|
+
if (this._seenMessageIds.has(msg.id)) continue;
|
|
2503
2704
|
const senderEncPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_encryption_key);
|
|
2504
2705
|
const ciphertext = (0, import_tweetnacl_util.decodeBase64)(msg.ciphertext);
|
|
2505
2706
|
const nonce = (0, import_tweetnacl_util.decodeBase64)(msg.nonce);
|
|
2506
|
-
|
|
2507
|
-
|
|
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
|
+
}
|
|
2770
|
+
let plaintextBytes = rawPlaintext;
|
|
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) {
|
|
2782
|
+
const unpadded = unpadMessage(rawPlaintext);
|
|
2783
|
+
if (unpadded.length < rawPlaintext.length) {
|
|
2784
|
+
plaintextBytes = unpadded;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
let content = (0, import_tweetnacl_util.encodeUTF8)(plaintextBytes);
|
|
2788
|
+
let senderDid = msg.from;
|
|
2789
|
+
if (wasSealed || !proto) {
|
|
2790
|
+
const unsealed = unsealEnvelope(content);
|
|
2791
|
+
if (unsealed) {
|
|
2792
|
+
content = unsealed.msg;
|
|
2793
|
+
senderDid = unsealed.from;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2508
2796
|
let signatureValid = false;
|
|
2509
2797
|
try {
|
|
2510
2798
|
const senderSignPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_signing_key);
|
|
2511
2799
|
const signatureBytes = (0, import_tweetnacl_util.decodeBase64)(msg.signature);
|
|
2512
2800
|
const envelopeStr = msg.envelope || JSON.stringify({
|
|
2513
|
-
from:
|
|
2801
|
+
from: senderDid,
|
|
2514
2802
|
to: msg.to,
|
|
2515
2803
|
timestamp: msg.timestamp,
|
|
2516
2804
|
nonce: msg.nonce,
|
|
@@ -2524,11 +2812,20 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2524
2812
|
} catch {
|
|
2525
2813
|
signatureValid = false;
|
|
2526
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
|
+
}
|
|
2527
2824
|
decrypted.push({
|
|
2528
2825
|
id: msg.id,
|
|
2529
|
-
from:
|
|
2826
|
+
from: senderDid,
|
|
2530
2827
|
to: msg.to,
|
|
2531
|
-
content
|
|
2828
|
+
content,
|
|
2532
2829
|
contentType: msg.content_type,
|
|
2533
2830
|
messageType: msg.message_type || "text",
|
|
2534
2831
|
threadId: msg.thread_id,
|
|
@@ -2538,6 +2835,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2538
2835
|
expiresAt: msg.expires_at
|
|
2539
2836
|
});
|
|
2540
2837
|
} catch {
|
|
2838
|
+
this._decryptFailCount++;
|
|
2541
2839
|
}
|
|
2542
2840
|
}
|
|
2543
2841
|
return decrypted;
|
|
@@ -2588,9 +2886,19 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2588
2886
|
* Look up an agent's public profile and keys.
|
|
2589
2887
|
*/
|
|
2590
2888
|
async getIdentity(did) {
|
|
2889
|
+
const cached = this._identityCache.get(did);
|
|
2890
|
+
if (cached && Date.now() - cached.cachedAt < 3e5) {
|
|
2891
|
+
return cached.profile;
|
|
2892
|
+
}
|
|
2591
2893
|
const res = await fetch(`${this.baseUrl}/v1/agent/identity/${did}`);
|
|
2592
2894
|
if (!res.ok) return null;
|
|
2593
|
-
|
|
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;
|
|
2594
2902
|
}
|
|
2595
2903
|
/**
|
|
2596
2904
|
* Search for agents by name or capability.
|
|
@@ -2650,10 +2958,14 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2650
2958
|
* Delete a webhook.
|
|
2651
2959
|
*/
|
|
2652
2960
|
async deleteWebhook(webhookId) {
|
|
2653
|
-
await fetch(`${this.baseUrl}/v1/agent/webhooks/${webhookId}`, {
|
|
2961
|
+
const res = await fetch(`${this.baseUrl}/v1/agent/webhooks/${webhookId}`, {
|
|
2654
2962
|
method: "DELETE",
|
|
2655
2963
|
headers: { "X-Agent-Key": this.apiKey }
|
|
2656
2964
|
});
|
|
2965
|
+
if (!res.ok) {
|
|
2966
|
+
const err = await res.json().catch(() => ({}));
|
|
2967
|
+
throw new Error(`Webhook delete failed: ${err.error || res.statusText}`);
|
|
2968
|
+
}
|
|
2657
2969
|
}
|
|
2658
2970
|
/**
|
|
2659
2971
|
* Verify a webhook payload signature (for use in your webhook handler).
|
|
@@ -3271,13 +3583,21 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3271
3583
|
* The relay finds matching agents and creates individual tasks for each.
|
|
3272
3584
|
*/
|
|
3273
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);
|
|
3274
3593
|
const res = await fetch(`${this.baseUrl}/v1/agent/tasks/broadcast`, {
|
|
3275
3594
|
method: "POST",
|
|
3276
3595
|
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
3277
3596
|
body: JSON.stringify({
|
|
3278
3597
|
capability: options.capability,
|
|
3279
|
-
encrypted_input: (0, import_tweetnacl_util.encodeBase64)(
|
|
3280
|
-
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),
|
|
3281
3601
|
priority: options.priority,
|
|
3282
3602
|
max_agents: options.maxAgents,
|
|
3283
3603
|
min_trust_level: options.minTrustLevel,
|
|
@@ -3347,16 +3667,30 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3347
3667
|
// ─── Memory Store ──────────────────────────────────────────────────────────
|
|
3348
3668
|
/**
|
|
3349
3669
|
* Store an encrypted key-value pair in persistent memory.
|
|
3350
|
-
* 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.
|
|
3351
3672
|
*/
|
|
3352
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);
|
|
3353
3686
|
const res = await fetch(`${this.baseUrl}/v1/agent/memory/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`, {
|
|
3354
3687
|
method: "PUT",
|
|
3355
3688
|
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
3356
3689
|
body: JSON.stringify({
|
|
3357
|
-
value,
|
|
3358
|
-
value_type: options?.valueType || (typeof value === "object" ? "json" : typeof value)
|
|
3359
|
-
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)
|
|
3360
3694
|
})
|
|
3361
3695
|
});
|
|
3362
3696
|
if (!res.ok) throw new Error(`Memory set failed: ${res.status} ${await res.text()}`);
|
|
@@ -3364,7 +3698,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3364
3698
|
}
|
|
3365
3699
|
/**
|
|
3366
3700
|
* Retrieve a value from persistent memory.
|
|
3367
|
-
* Decrypted
|
|
3701
|
+
* Decrypted CLIENT-SIDE — relay never sees plaintext.
|
|
3368
3702
|
*/
|
|
3369
3703
|
async memoryGet(namespace, key) {
|
|
3370
3704
|
const res = await fetch(`${this.baseUrl}/v1/agent/memory/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`, {
|
|
@@ -3372,7 +3706,28 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3372
3706
|
});
|
|
3373
3707
|
if (res.status === 404) return null;
|
|
3374
3708
|
if (!res.ok) throw new Error(`Memory get failed: ${res.status} ${await res.text()}`);
|
|
3375
|
-
|
|
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;
|
|
3376
3731
|
}
|
|
3377
3732
|
/**
|
|
3378
3733
|
* Delete a key from persistent memory.
|
|
@@ -3788,6 +4143,96 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3788
4143
|
}
|
|
3789
4144
|
throw lastError || new Error("Send failed after retries");
|
|
3790
4145
|
}
|
|
4146
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4147
|
+
// OFFLINE QUEUE — Resilience against relay downtime
|
|
4148
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4149
|
+
/**
|
|
4150
|
+
* Drain the offline message queue — retry sending queued messages.
|
|
4151
|
+
* Call this when connectivity is restored.
|
|
4152
|
+
* Returns: number of messages successfully sent.
|
|
4153
|
+
*/
|
|
4154
|
+
async drainQueue() {
|
|
4155
|
+
let sent = 0;
|
|
4156
|
+
let failed = 0;
|
|
4157
|
+
const remaining = [];
|
|
4158
|
+
for (const item of this._offlineQueue) {
|
|
4159
|
+
if (Date.now() - item.timestamp > 864e5) {
|
|
4160
|
+
failed++;
|
|
4161
|
+
continue;
|
|
4162
|
+
}
|
|
4163
|
+
try {
|
|
4164
|
+
await this.send(item.recipientDid, item.message, item.options);
|
|
4165
|
+
sent++;
|
|
4166
|
+
} catch {
|
|
4167
|
+
remaining.push(item);
|
|
4168
|
+
failed++;
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
this._offlineQueue = remaining;
|
|
4172
|
+
return { sent, failed, remaining: remaining.length };
|
|
4173
|
+
}
|
|
4174
|
+
/** Number of messages waiting in the offline queue */
|
|
4175
|
+
get queueLength() {
|
|
4176
|
+
return this._offlineQueue.length;
|
|
4177
|
+
}
|
|
4178
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4179
|
+
// SECURITY REPORT — Transparent threat model
|
|
4180
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4181
|
+
/**
|
|
4182
|
+
* Returns what the relay can and cannot see about this agent.
|
|
4183
|
+
* Call this to understand your threat model. Total transparency.
|
|
4184
|
+
*/
|
|
4185
|
+
threatModel() {
|
|
4186
|
+
return {
|
|
4187
|
+
relayCanSee: [
|
|
4188
|
+
"Your DID (public identifier)",
|
|
4189
|
+
"Who you message (recipient DIDs)",
|
|
4190
|
+
"When you message (timestamps)",
|
|
4191
|
+
"Message types (text, task-request, etc.)",
|
|
4192
|
+
"Thread structure (which messages are replies)",
|
|
4193
|
+
"Channel membership",
|
|
4194
|
+
"Capability registrations",
|
|
4195
|
+
"Online/offline status",
|
|
4196
|
+
"Approximate message size (even with padding, bounded to power-of-2)"
|
|
4197
|
+
],
|
|
4198
|
+
relayCannotSee: [
|
|
4199
|
+
"Message content (E2E encrypted \u2014 nacl.secretbox with ratchet-derived per-message keys)",
|
|
4200
|
+
"Private keys (generated and stored client-side only)",
|
|
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)",
|
|
4203
|
+
...this.sealedSender ? ["Sender identity (sealed inside ciphertext)"] : []
|
|
4204
|
+
],
|
|
4205
|
+
protections: [
|
|
4206
|
+
"Hash ratchet forward secrecy \u2014 per-message key derivation, old keys deleted",
|
|
4207
|
+
"X25519 key exchange + XSalsa20-Poly1305 authenticated encryption",
|
|
4208
|
+
"Ed25519 signatures on every message (envelope + ciphertext hash)",
|
|
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)",
|
|
4215
|
+
...this.paddingEnabled ? ["Message padding to power-of-2 boundary (traffic analysis resistance)"] : [],
|
|
4216
|
+
...this.sealedSender ? ["Sealed sender (relay cannot see who sent a message)"] : [],
|
|
4217
|
+
...this.requireSignatures ? ["Strict signature enforcement (reject unsigned/invalid messages)"] : [],
|
|
4218
|
+
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays)`] : [],
|
|
4219
|
+
"Auto-retry with exponential backoff",
|
|
4220
|
+
"Offline message queue",
|
|
4221
|
+
"did:key interoperability (W3C standard DID format)"
|
|
4222
|
+
],
|
|
4223
|
+
gaps: [
|
|
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)",
|
|
4226
|
+
"Channel encryption is server-side (relay holds channel keys, NOT true E2E)",
|
|
4227
|
+
"Metadata (who, when, thread structure) visible to relay operator",
|
|
4228
|
+
"Single relay architecture (no onion routing, no mix network)",
|
|
4229
|
+
"Ed25519 signatures are non-repudiable (no deniable messaging)",
|
|
4230
|
+
"No async key agreement (no X3DH prekeys)",
|
|
4231
|
+
"Polling-based (no WebSocket real-time transport)",
|
|
4232
|
+
"Ratchet state is in-memory (lost on process restart \u2014 export credentials to persist)"
|
|
4233
|
+
]
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
3791
4236
|
};
|
|
3792
4237
|
var Conversation = class {
|
|
3793
4238
|
/** @internal */
|
|
@@ -3810,6 +4255,9 @@ var Conversation = class {
|
|
|
3810
4255
|
replyTo: this._lastMessageId || void 0
|
|
3811
4256
|
});
|
|
3812
4257
|
this._lastMessageId = result.id;
|
|
4258
|
+
if (this._messageHistory.length >= 1e3) {
|
|
4259
|
+
this._messageHistory.splice(0, this._messageHistory.length - 999);
|
|
4260
|
+
}
|
|
3813
4261
|
this._messageHistory.push({
|
|
3814
4262
|
id: result.id,
|
|
3815
4263
|
from: this.agent.did,
|
|
@@ -3884,14 +4332,23 @@ var Conversation = class {
|
|
|
3884
4332
|
async waitForReply(timeoutMs = 3e4) {
|
|
3885
4333
|
return new Promise((resolve, reject) => {
|
|
3886
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
|
+
};
|
|
3887
4343
|
const timeout = setTimeout(() => {
|
|
3888
4344
|
if (!resolved) {
|
|
3889
|
-
|
|
4345
|
+
cleanup();
|
|
3890
4346
|
reject(new Error(`No reply received within ${timeoutMs}ms`));
|
|
3891
4347
|
}
|
|
3892
4348
|
}, timeoutMs);
|
|
3893
4349
|
const check = async () => {
|
|
3894
|
-
|
|
4350
|
+
if (resolved) return;
|
|
4351
|
+
try {
|
|
3895
4352
|
const messages = await this.agent.receive({
|
|
3896
4353
|
from: this.peerDid,
|
|
3897
4354
|
threadId: this.threadId,
|
|
@@ -3899,9 +4356,12 @@ var Conversation = class {
|
|
|
3899
4356
|
limit: 1
|
|
3900
4357
|
});
|
|
3901
4358
|
if (messages.length > 0 && !resolved) {
|
|
3902
|
-
resolved = true;
|
|
3903
4359
|
clearTimeout(timeout);
|
|
4360
|
+
cleanup();
|
|
3904
4361
|
const msg = messages[0];
|
|
4362
|
+
if (this._messageHistory.length >= 1e3) {
|
|
4363
|
+
this._messageHistory.splice(0, this._messageHistory.length - 999);
|
|
4364
|
+
}
|
|
3905
4365
|
this._messageHistory.push({
|
|
3906
4366
|
id: msg.id,
|
|
3907
4367
|
from: msg.from,
|
|
@@ -3916,16 +4376,19 @@ var Conversation = class {
|
|
|
3916
4376
|
resolve(msg);
|
|
3917
4377
|
return;
|
|
3918
4378
|
}
|
|
3919
|
-
|
|
4379
|
+
} catch (err) {
|
|
4380
|
+
if (!resolved) {
|
|
4381
|
+
clearTimeout(timeout);
|
|
4382
|
+
cleanup();
|
|
4383
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
4384
|
+
return;
|
|
4385
|
+
}
|
|
3920
4386
|
}
|
|
3921
|
-
};
|
|
3922
|
-
check().catch((err) => {
|
|
3923
4387
|
if (!resolved) {
|
|
3924
|
-
|
|
3925
|
-
clearTimeout(timeout);
|
|
3926
|
-
reject(err);
|
|
4388
|
+
pollTimer = setTimeout(check, 1500);
|
|
3927
4389
|
}
|
|
3928
|
-
}
|
|
4390
|
+
};
|
|
4391
|
+
check();
|
|
3929
4392
|
});
|
|
3930
4393
|
}
|
|
3931
4394
|
/**
|