@voidly/agent-sdk 1.8.2 → 1.9.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 +207 -3
- package/dist/index.d.ts +207 -3
- package/dist/index.js +445 -26
- package/dist/index.mjs +444 -26
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2326,6 +2326,7 @@ var require_nacl_util = __commonJS({
|
|
|
2326
2326
|
// src/index.ts
|
|
2327
2327
|
var index_exports = {};
|
|
2328
2328
|
__export(index_exports, {
|
|
2329
|
+
Conversation: () => Conversation,
|
|
2329
2330
|
VoidlyAgent: () => VoidlyAgent,
|
|
2330
2331
|
decodeBase64: () => import_tweetnacl_util.decodeBase64,
|
|
2331
2332
|
decodeUTF8: () => import_tweetnacl_util.decodeUTF8,
|
|
@@ -2347,11 +2348,16 @@ async function sha256(data) {
|
|
|
2347
2348
|
}
|
|
2348
2349
|
var VoidlyAgent = class _VoidlyAgent {
|
|
2349
2350
|
constructor(identity, config) {
|
|
2351
|
+
this._pinnedDids = /* @__PURE__ */ new Set();
|
|
2352
|
+
this._listeners = /* @__PURE__ */ new Set();
|
|
2353
|
+
this._conversations = /* @__PURE__ */ new Map();
|
|
2350
2354
|
this.did = identity.did;
|
|
2351
2355
|
this.apiKey = identity.apiKey;
|
|
2352
2356
|
this.signingKeyPair = identity.signingKeyPair;
|
|
2353
2357
|
this.encryptionKeyPair = identity.encryptionKeyPair;
|
|
2354
2358
|
this.baseUrl = config?.baseUrl || "https://api.voidly.ai";
|
|
2359
|
+
this.autoPin = config?.autoPin !== false;
|
|
2360
|
+
this.defaultRetries = config?.retries ?? 3;
|
|
2355
2361
|
}
|
|
2356
2362
|
// ─── Factory Methods ────────────────────────────────────────────────────────
|
|
2357
2363
|
/**
|
|
@@ -2418,14 +2424,23 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2418
2424
|
}
|
|
2419
2425
|
// ─── Messaging ──────────────────────────────────────────────────────────────
|
|
2420
2426
|
/**
|
|
2421
|
-
* Send an E2E encrypted message
|
|
2422
|
-
*
|
|
2427
|
+
* Send an E2E encrypted message with automatic retry and transparent TOFU.
|
|
2428
|
+
* Encryption happens locally — the relay NEVER sees plaintext or private keys.
|
|
2429
|
+
*
|
|
2430
|
+
* Features:
|
|
2431
|
+
* - **Auto-retry** with exponential backoff on transient failures
|
|
2432
|
+
* - **Transparent TOFU** — automatically pins recipient keys on first contact
|
|
2433
|
+
* - **Key verification** — warns if pinned keys have changed (potential MitM)
|
|
2423
2434
|
*/
|
|
2424
2435
|
async send(recipientDid, message, options = {}) {
|
|
2436
|
+
const maxRetries = options.retries ?? this.defaultRetries;
|
|
2425
2437
|
const profile = await this.getIdentity(recipientDid);
|
|
2426
2438
|
if (!profile) {
|
|
2427
2439
|
throw new Error(`Recipient ${recipientDid} not found`);
|
|
2428
2440
|
}
|
|
2441
|
+
if (this.autoPin && !options.skipPin) {
|
|
2442
|
+
await this._autoPinKeys(recipientDid);
|
|
2443
|
+
}
|
|
2429
2444
|
const recipientPubKey = (0, import_tweetnacl_util.decodeBase64)(profile.encryption_public_key);
|
|
2430
2445
|
const messageBytes = (0, import_tweetnacl_util.decodeUTF8)(message);
|
|
2431
2446
|
const nonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.box.nonceLength);
|
|
@@ -2441,31 +2456,27 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2441
2456
|
ciphertext_hash: await sha256((0, import_tweetnacl_util.encodeBase64)(ciphertext))
|
|
2442
2457
|
});
|
|
2443
2458
|
const signature = import_tweetnacl.default.sign.detached((0, import_tweetnacl_util.decodeUTF8)(envelopeData), this.signingKeyPair.secretKey);
|
|
2444
|
-
const
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2459
|
+
const payload = {
|
|
2460
|
+
to: recipientDid,
|
|
2461
|
+
ciphertext: (0, import_tweetnacl_util.encodeBase64)(ciphertext),
|
|
2462
|
+
nonce: (0, import_tweetnacl_util.encodeBase64)(nonce),
|
|
2463
|
+
signature: (0, import_tweetnacl_util.encodeBase64)(signature),
|
|
2464
|
+
envelope: envelopeData,
|
|
2465
|
+
content_type: options.contentType || "text/plain",
|
|
2466
|
+
message_type: options.messageType || "text",
|
|
2467
|
+
thread_id: options.threadId,
|
|
2468
|
+
reply_to: options.replyTo,
|
|
2469
|
+
ttl: options.ttl
|
|
2470
|
+
};
|
|
2471
|
+
const raw = await this._fetchWithRetry(
|
|
2472
|
+
`${this.baseUrl}/v1/agent/send/encrypted`,
|
|
2473
|
+
{
|
|
2474
|
+
method: "POST",
|
|
2475
|
+
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
2476
|
+
body: JSON.stringify(payload)
|
|
2449
2477
|
},
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
ciphertext: (0, import_tweetnacl_util.encodeBase64)(ciphertext),
|
|
2453
|
-
nonce: (0, import_tweetnacl_util.encodeBase64)(nonce),
|
|
2454
|
-
signature: (0, import_tweetnacl_util.encodeBase64)(signature),
|
|
2455
|
-
envelope: envelopeData,
|
|
2456
|
-
// Pass signed envelope so receiver can verify
|
|
2457
|
-
content_type: options.contentType || "text/plain",
|
|
2458
|
-
message_type: options.messageType || "text",
|
|
2459
|
-
thread_id: options.threadId,
|
|
2460
|
-
reply_to: options.replyTo,
|
|
2461
|
-
ttl: options.ttl
|
|
2462
|
-
})
|
|
2463
|
-
});
|
|
2464
|
-
if (!res.ok) {
|
|
2465
|
-
const err = await res.json().catch(() => ({}));
|
|
2466
|
-
throw new Error(`Send failed: ${err.error?.message || err.error || res.statusText}`);
|
|
2467
|
-
}
|
|
2468
|
-
const raw = await res.json();
|
|
2478
|
+
{ maxRetries, baseDelay: 500, maxDelay: 1e4 }
|
|
2479
|
+
);
|
|
2469
2480
|
return {
|
|
2470
2481
|
id: raw.id,
|
|
2471
2482
|
from: raw.from,
|
|
@@ -3539,9 +3550,417 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3539
3550
|
if (!res.ok) throw new Error(`Key verify failed: ${res.status}`);
|
|
3540
3551
|
return res.json();
|
|
3541
3552
|
}
|
|
3553
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3554
|
+
// LISTEN — Event-Driven Message Receiving
|
|
3555
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3556
|
+
/**
|
|
3557
|
+
* Listen for incoming messages with an event-driven callback.
|
|
3558
|
+
* Uses adaptive polling — speeds up when messages are flowing, slows down when idle.
|
|
3559
|
+
* Automatically sends heartbeat pings to signal the agent is online.
|
|
3560
|
+
*
|
|
3561
|
+
* @example
|
|
3562
|
+
* ```ts
|
|
3563
|
+
* // Simple listener
|
|
3564
|
+
* const handle = agent.listen((msg) => {
|
|
3565
|
+
* console.log(`${msg.from}: ${msg.content}`);
|
|
3566
|
+
* });
|
|
3567
|
+
*
|
|
3568
|
+
* // Stop after 60 seconds
|
|
3569
|
+
* setTimeout(() => handle.stop(), 60000);
|
|
3570
|
+
*
|
|
3571
|
+
* // With options
|
|
3572
|
+
* const handle = agent.listen(
|
|
3573
|
+
* (msg) => console.log(msg.content),
|
|
3574
|
+
* {
|
|
3575
|
+
* interval: 1000, // poll every 1s
|
|
3576
|
+
* from: 'did:voidly:x', // only from this agent
|
|
3577
|
+
* threadId: 'conv-1', // only this thread
|
|
3578
|
+
* adaptive: true, // slow down when idle
|
|
3579
|
+
* heartbeat: true, // send pings
|
|
3580
|
+
* }
|
|
3581
|
+
* );
|
|
3582
|
+
* ```
|
|
3583
|
+
*/
|
|
3584
|
+
listen(onMessage, options = {}, onError) {
|
|
3585
|
+
const interval = Math.max(options.interval || 2e3, 500);
|
|
3586
|
+
const adaptive = options.adaptive !== false;
|
|
3587
|
+
const autoMarkRead = options.autoMarkRead !== false;
|
|
3588
|
+
const unreadOnly = options.unreadOnly !== false;
|
|
3589
|
+
const heartbeat = options.heartbeat !== false;
|
|
3590
|
+
const heartbeatInterval = options.heartbeatInterval || 6e4;
|
|
3591
|
+
let active = true;
|
|
3592
|
+
let currentInterval = interval;
|
|
3593
|
+
let consecutiveEmpty = 0;
|
|
3594
|
+
let lastSeen;
|
|
3595
|
+
let timer = null;
|
|
3596
|
+
let heartbeatTimer = null;
|
|
3597
|
+
const handle = {
|
|
3598
|
+
stop: () => {
|
|
3599
|
+
active = false;
|
|
3600
|
+
if (timer) clearTimeout(timer);
|
|
3601
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
3602
|
+
this._listeners.delete(handle);
|
|
3603
|
+
},
|
|
3604
|
+
get active() {
|
|
3605
|
+
return active;
|
|
3606
|
+
}
|
|
3607
|
+
};
|
|
3608
|
+
this._listeners.add(handle);
|
|
3609
|
+
if (heartbeat) {
|
|
3610
|
+
heartbeatTimer = setInterval(async () => {
|
|
3611
|
+
if (!active) return;
|
|
3612
|
+
try {
|
|
3613
|
+
await this.ping();
|
|
3614
|
+
} catch {
|
|
3615
|
+
}
|
|
3616
|
+
}, heartbeatInterval);
|
|
3617
|
+
this.ping().catch(() => {
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
const poll = async () => {
|
|
3621
|
+
if (!active || options.signal?.aborted) {
|
|
3622
|
+
handle.stop();
|
|
3623
|
+
return;
|
|
3624
|
+
}
|
|
3625
|
+
try {
|
|
3626
|
+
const messages = await this.receive({
|
|
3627
|
+
since: lastSeen,
|
|
3628
|
+
from: options.from,
|
|
3629
|
+
threadId: options.threadId,
|
|
3630
|
+
messageType: options.messageType,
|
|
3631
|
+
unreadOnly,
|
|
3632
|
+
limit: 50
|
|
3633
|
+
});
|
|
3634
|
+
if (messages.length > 0) {
|
|
3635
|
+
consecutiveEmpty = 0;
|
|
3636
|
+
if (adaptive) currentInterval = Math.max(interval / 2, 500);
|
|
3637
|
+
for (const msg of messages) {
|
|
3638
|
+
try {
|
|
3639
|
+
await onMessage(msg);
|
|
3640
|
+
if (autoMarkRead) {
|
|
3641
|
+
await this.markRead(msg.id).catch(() => {
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
} catch (err) {
|
|
3645
|
+
if (onError) onError(err instanceof Error ? err : new Error(String(err)));
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
lastSeen = messages[messages.length - 1].timestamp;
|
|
3649
|
+
} else {
|
|
3650
|
+
consecutiveEmpty++;
|
|
3651
|
+
if (adaptive && consecutiveEmpty > 3) {
|
|
3652
|
+
currentInterval = Math.min(currentInterval * 1.5, interval * 4);
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
} catch (err) {
|
|
3656
|
+
if (onError) onError(err instanceof Error ? err : new Error(String(err)));
|
|
3657
|
+
currentInterval = Math.min(currentInterval * 2, interval * 8);
|
|
3658
|
+
}
|
|
3659
|
+
if (active && !options.signal?.aborted) {
|
|
3660
|
+
timer = setTimeout(poll, currentInterval);
|
|
3661
|
+
}
|
|
3662
|
+
};
|
|
3663
|
+
poll();
|
|
3664
|
+
return handle;
|
|
3665
|
+
}
|
|
3666
|
+
/**
|
|
3667
|
+
* Listen for messages as an async iterator.
|
|
3668
|
+
* Enables `for await` syntax for message processing.
|
|
3669
|
+
*
|
|
3670
|
+
* @example
|
|
3671
|
+
* ```ts
|
|
3672
|
+
* for await (const msg of agent.messages({ unreadOnly: true })) {
|
|
3673
|
+
* console.log(`${msg.from}: ${msg.content}`);
|
|
3674
|
+
* if (msg.content === 'quit') break;
|
|
3675
|
+
* }
|
|
3676
|
+
* ```
|
|
3677
|
+
*/
|
|
3678
|
+
async *messages(options = {}) {
|
|
3679
|
+
const queue = [];
|
|
3680
|
+
let resolve = null;
|
|
3681
|
+
let done = false;
|
|
3682
|
+
const handle = this.listen(
|
|
3683
|
+
(msg) => {
|
|
3684
|
+
queue.push(msg);
|
|
3685
|
+
if (resolve) {
|
|
3686
|
+
resolve();
|
|
3687
|
+
resolve = null;
|
|
3688
|
+
}
|
|
3689
|
+
},
|
|
3690
|
+
{ ...options, autoMarkRead: options.autoMarkRead !== false }
|
|
3691
|
+
);
|
|
3692
|
+
options.signal?.addEventListener("abort", () => {
|
|
3693
|
+
done = true;
|
|
3694
|
+
handle.stop();
|
|
3695
|
+
if (resolve) {
|
|
3696
|
+
resolve();
|
|
3697
|
+
resolve = null;
|
|
3698
|
+
}
|
|
3699
|
+
});
|
|
3700
|
+
try {
|
|
3701
|
+
while (!done && !options.signal?.aborted) {
|
|
3702
|
+
if (queue.length > 0) {
|
|
3703
|
+
yield queue.shift();
|
|
3704
|
+
} else {
|
|
3705
|
+
await new Promise((r) => {
|
|
3706
|
+
resolve = r;
|
|
3707
|
+
});
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
} finally {
|
|
3711
|
+
handle.stop();
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
/**
|
|
3715
|
+
* Stop all active listeners. Useful for clean shutdown.
|
|
3716
|
+
*/
|
|
3717
|
+
stopAll() {
|
|
3718
|
+
for (const listener of this._listeners) {
|
|
3719
|
+
listener.stop();
|
|
3720
|
+
}
|
|
3721
|
+
this._listeners.clear();
|
|
3722
|
+
}
|
|
3723
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3724
|
+
// CONVERSATIONS — Thread Management
|
|
3725
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3726
|
+
/**
|
|
3727
|
+
* Start or resume a conversation with another agent.
|
|
3728
|
+
* Automatically manages thread IDs, message history, and reply chains.
|
|
3729
|
+
*
|
|
3730
|
+
* @example
|
|
3731
|
+
* ```ts
|
|
3732
|
+
* const conv = agent.conversation(otherDid);
|
|
3733
|
+
* await conv.say('Hello!');
|
|
3734
|
+
* await conv.say('How are you?');
|
|
3735
|
+
*
|
|
3736
|
+
* // Get full history
|
|
3737
|
+
* const history = await conv.history();
|
|
3738
|
+
*
|
|
3739
|
+
* // Listen for replies in this conversation
|
|
3740
|
+
* conv.onReply((msg) => {
|
|
3741
|
+
* console.log(`Reply: ${msg.content}`);
|
|
3742
|
+
* });
|
|
3743
|
+
* ```
|
|
3744
|
+
*/
|
|
3745
|
+
conversation(peerDid, threadId) {
|
|
3746
|
+
const tid = threadId || `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3747
|
+
const key = `${peerDid}:${tid}`;
|
|
3748
|
+
if (this._conversations.has(key)) {
|
|
3749
|
+
return this._conversations.get(key);
|
|
3750
|
+
}
|
|
3751
|
+
const conv = new Conversation(this, peerDid, tid);
|
|
3752
|
+
this._conversations.set(key, conv);
|
|
3753
|
+
return conv;
|
|
3754
|
+
}
|
|
3755
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3756
|
+
// INTERNAL — Retry, Auto-Pin
|
|
3757
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3758
|
+
/** @internal Auto-pin keys on first contact (TOFU) */
|
|
3759
|
+
async _autoPinKeys(did) {
|
|
3760
|
+
if (this._pinnedDids.has(did)) return;
|
|
3761
|
+
this._pinnedDids.add(did);
|
|
3762
|
+
try {
|
|
3763
|
+
const result = await this.pinKeys(did);
|
|
3764
|
+
if (result.key_changed && result.warning) {
|
|
3765
|
+
console.warn(`[voidly] \u26A0 Key change detected for ${did}: ${result.warning}`);
|
|
3766
|
+
}
|
|
3767
|
+
} catch {
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
/** @internal Fetch with exponential backoff retry */
|
|
3771
|
+
async _fetchWithRetry(url, init, retry = {}) {
|
|
3772
|
+
const maxRetries = retry.maxRetries ?? 3;
|
|
3773
|
+
const baseDelay = retry.baseDelay ?? 500;
|
|
3774
|
+
const maxDelay = retry.maxDelay ?? 1e4;
|
|
3775
|
+
let lastError = null;
|
|
3776
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
3777
|
+
try {
|
|
3778
|
+
const res = await fetch(url, init);
|
|
3779
|
+
if (res.ok) {
|
|
3780
|
+
return await res.json();
|
|
3781
|
+
}
|
|
3782
|
+
const err = await res.json().catch(() => ({}));
|
|
3783
|
+
const errMsg = err.error?.message || err.error || res.statusText;
|
|
3784
|
+
if (res.status >= 400 && res.status < 500) {
|
|
3785
|
+
throw new Error(`Send failed (${res.status}): ${errMsg}`);
|
|
3786
|
+
}
|
|
3787
|
+
lastError = new Error(`Send failed (${res.status}): ${errMsg}`);
|
|
3788
|
+
} catch (err) {
|
|
3789
|
+
if (err instanceof Error && err.message.startsWith("Send failed (4")) {
|
|
3790
|
+
throw err;
|
|
3791
|
+
}
|
|
3792
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
3793
|
+
}
|
|
3794
|
+
if (attempt < maxRetries) {
|
|
3795
|
+
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
|
|
3796
|
+
const jitter = delay * (0.5 + Math.random() * 0.5);
|
|
3797
|
+
await new Promise((r) => setTimeout(r, jitter));
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
throw lastError || new Error("Send failed after retries");
|
|
3801
|
+
}
|
|
3802
|
+
};
|
|
3803
|
+
var Conversation = class {
|
|
3804
|
+
/** @internal */
|
|
3805
|
+
constructor(agent, peerDid, threadId) {
|
|
3806
|
+
this._lastMessageId = null;
|
|
3807
|
+
this._messageHistory = [];
|
|
3808
|
+
this._listener = null;
|
|
3809
|
+
this._replyHandlers = [];
|
|
3810
|
+
this.agent = agent;
|
|
3811
|
+
this.peerDid = peerDid;
|
|
3812
|
+
this.threadId = threadId;
|
|
3813
|
+
}
|
|
3814
|
+
/**
|
|
3815
|
+
* Send a message in this conversation. Auto-threaded and auto-linked to previous message.
|
|
3816
|
+
*/
|
|
3817
|
+
async say(content, options) {
|
|
3818
|
+
const result = await this.agent.send(this.peerDid, content, {
|
|
3819
|
+
...options,
|
|
3820
|
+
threadId: this.threadId,
|
|
3821
|
+
replyTo: this._lastMessageId || void 0
|
|
3822
|
+
});
|
|
3823
|
+
this._lastMessageId = result.id;
|
|
3824
|
+
this._messageHistory.push({
|
|
3825
|
+
id: result.id,
|
|
3826
|
+
from: this.agent.did,
|
|
3827
|
+
content,
|
|
3828
|
+
timestamp: result.timestamp,
|
|
3829
|
+
signatureValid: true,
|
|
3830
|
+
messageType: options?.messageType || "text"
|
|
3831
|
+
});
|
|
3832
|
+
return result;
|
|
3833
|
+
}
|
|
3834
|
+
/**
|
|
3835
|
+
* Get conversation history (both sent and received messages in this thread).
|
|
3836
|
+
*/
|
|
3837
|
+
async history(options) {
|
|
3838
|
+
const received = await this.agent.receive({
|
|
3839
|
+
threadId: this.threadId,
|
|
3840
|
+
from: this.peerDid,
|
|
3841
|
+
limit: options?.limit || 100
|
|
3842
|
+
});
|
|
3843
|
+
const all = [
|
|
3844
|
+
...this._messageHistory,
|
|
3845
|
+
...received.map((m) => ({
|
|
3846
|
+
id: m.id,
|
|
3847
|
+
from: m.from,
|
|
3848
|
+
content: m.content,
|
|
3849
|
+
timestamp: m.timestamp,
|
|
3850
|
+
signatureValid: m.signatureValid,
|
|
3851
|
+
messageType: m.messageType
|
|
3852
|
+
}))
|
|
3853
|
+
];
|
|
3854
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3855
|
+
return all.filter((m) => {
|
|
3856
|
+
if (seen.has(m.id)) return false;
|
|
3857
|
+
seen.add(m.id);
|
|
3858
|
+
return true;
|
|
3859
|
+
}).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
3860
|
+
}
|
|
3861
|
+
/**
|
|
3862
|
+
* Register a callback for replies in this conversation.
|
|
3863
|
+
*/
|
|
3864
|
+
onReply(handler) {
|
|
3865
|
+
this._replyHandlers.push(handler);
|
|
3866
|
+
if (!this._listener) {
|
|
3867
|
+
this._listener = this.agent.listen(
|
|
3868
|
+
async (msg) => {
|
|
3869
|
+
this._messageHistory.push({
|
|
3870
|
+
id: msg.id,
|
|
3871
|
+
from: msg.from,
|
|
3872
|
+
content: msg.content,
|
|
3873
|
+
timestamp: msg.timestamp,
|
|
3874
|
+
signatureValid: msg.signatureValid,
|
|
3875
|
+
messageType: msg.messageType
|
|
3876
|
+
});
|
|
3877
|
+
this._lastMessageId = msg.id;
|
|
3878
|
+
for (const h of this._replyHandlers) {
|
|
3879
|
+
try {
|
|
3880
|
+
await h(msg);
|
|
3881
|
+
} catch {
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
},
|
|
3885
|
+
{ from: this.peerDid, threadId: this.threadId, autoMarkRead: true }
|
|
3886
|
+
);
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
/**
|
|
3890
|
+
* Wait for the next reply in this conversation (Promise-based).
|
|
3891
|
+
*
|
|
3892
|
+
* @param timeoutMs - Maximum time to wait (default: 30000ms)
|
|
3893
|
+
* @throws Error on timeout
|
|
3894
|
+
*/
|
|
3895
|
+
async waitForReply(timeoutMs = 3e4) {
|
|
3896
|
+
return new Promise((resolve, reject) => {
|
|
3897
|
+
let resolved = false;
|
|
3898
|
+
const timeout = setTimeout(() => {
|
|
3899
|
+
if (!resolved) {
|
|
3900
|
+
resolved = true;
|
|
3901
|
+
reject(new Error(`No reply received within ${timeoutMs}ms`));
|
|
3902
|
+
}
|
|
3903
|
+
}, timeoutMs);
|
|
3904
|
+
const check = async () => {
|
|
3905
|
+
while (!resolved) {
|
|
3906
|
+
const messages = await this.agent.receive({
|
|
3907
|
+
from: this.peerDid,
|
|
3908
|
+
threadId: this.threadId,
|
|
3909
|
+
unreadOnly: true,
|
|
3910
|
+
limit: 1
|
|
3911
|
+
});
|
|
3912
|
+
if (messages.length > 0 && !resolved) {
|
|
3913
|
+
resolved = true;
|
|
3914
|
+
clearTimeout(timeout);
|
|
3915
|
+
const msg = messages[0];
|
|
3916
|
+
this._messageHistory.push({
|
|
3917
|
+
id: msg.id,
|
|
3918
|
+
from: msg.from,
|
|
3919
|
+
content: msg.content,
|
|
3920
|
+
timestamp: msg.timestamp,
|
|
3921
|
+
signatureValid: msg.signatureValid,
|
|
3922
|
+
messageType: msg.messageType
|
|
3923
|
+
});
|
|
3924
|
+
this._lastMessageId = msg.id;
|
|
3925
|
+
await this.agent.markRead(msg.id).catch(() => {
|
|
3926
|
+
});
|
|
3927
|
+
resolve(msg);
|
|
3928
|
+
return;
|
|
3929
|
+
}
|
|
3930
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
3931
|
+
}
|
|
3932
|
+
};
|
|
3933
|
+
check().catch((err) => {
|
|
3934
|
+
if (!resolved) {
|
|
3935
|
+
resolved = true;
|
|
3936
|
+
clearTimeout(timeout);
|
|
3937
|
+
reject(err);
|
|
3938
|
+
}
|
|
3939
|
+
});
|
|
3940
|
+
});
|
|
3941
|
+
}
|
|
3942
|
+
/**
|
|
3943
|
+
* Stop listening for replies and clean up.
|
|
3944
|
+
*/
|
|
3945
|
+
close() {
|
|
3946
|
+
if (this._listener) {
|
|
3947
|
+
this._listener.stop();
|
|
3948
|
+
this._listener = null;
|
|
3949
|
+
}
|
|
3950
|
+
this._replyHandlers = [];
|
|
3951
|
+
}
|
|
3952
|
+
/** Number of messages tracked locally */
|
|
3953
|
+
get length() {
|
|
3954
|
+
return this._messageHistory.length;
|
|
3955
|
+
}
|
|
3956
|
+
/** The last message in this conversation */
|
|
3957
|
+
get lastMessage() {
|
|
3958
|
+
return this._messageHistory.length > 0 ? this._messageHistory[this._messageHistory.length - 1] : null;
|
|
3959
|
+
}
|
|
3542
3960
|
};
|
|
3543
3961
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3544
3962
|
0 && (module.exports = {
|
|
3963
|
+
Conversation,
|
|
3545
3964
|
VoidlyAgent,
|
|
3546
3965
|
decodeBase64,
|
|
3547
3966
|
decodeUTF8,
|