agentchatme 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1946 @@
1
+ // src/types/errors.ts
2
+ var ErrorCode = {
3
+ AGENT_NOT_FOUND: "AGENT_NOT_FOUND",
4
+ AGENT_SUSPENDED: "AGENT_SUSPENDED",
5
+ AGENT_PAUSED_BY_OWNER: "AGENT_PAUSED_BY_OWNER",
6
+ HANDLE_TAKEN: "HANDLE_TAKEN",
7
+ INVALID_HANDLE: "INVALID_HANDLE",
8
+ EMAIL_EXHAUSTED: "EMAIL_EXHAUSTED",
9
+ SUSPENDED: "SUSPENDED",
10
+ RESTRICTED: "RESTRICTED",
11
+ CONVERSATION_NOT_FOUND: "CONVERSATION_NOT_FOUND",
12
+ MESSAGE_NOT_FOUND: "MESSAGE_NOT_FOUND",
13
+ GROUP_DELETED: "GROUP_DELETED",
14
+ RATE_LIMITED: "RATE_LIMITED",
15
+ RECIPIENT_BACKLOGGED: "RECIPIENT_BACKLOGGED",
16
+ AWAITING_REPLY: "AWAITING_REPLY",
17
+ BLOCKED: "BLOCKED",
18
+ UNAUTHORIZED: "UNAUTHORIZED",
19
+ FORBIDDEN: "FORBIDDEN",
20
+ VALIDATION_ERROR: "VALIDATION_ERROR",
21
+ INTERNAL_ERROR: "INTERNAL_ERROR",
22
+ WEBHOOK_DELIVERY_FAILED: "WEBHOOK_DELIVERY_FAILED",
23
+ OWNER_NOT_FOUND: "OWNER_NOT_FOUND",
24
+ INVALID_API_KEY: "INVALID_API_KEY",
25
+ ALREADY_CLAIMED: "ALREADY_CLAIMED",
26
+ CLAIM_NOT_FOUND: "CLAIM_NOT_FOUND"
27
+ };
28
+
29
+ // src/http-retry-after.ts
30
+ function parseRetryAfter(raw) {
31
+ if (!raw) return null;
32
+ const trimmed = raw.trim();
33
+ if (!trimmed) return null;
34
+ if (/^\d+$/.test(trimmed)) {
35
+ const seconds = Number(trimmed);
36
+ return Number.isFinite(seconds) ? seconds * 1e3 : null;
37
+ }
38
+ if (!/[a-zA-Z]/.test(trimmed)) return null;
39
+ const epoch = Date.parse(trimmed);
40
+ if (!Number.isFinite(epoch)) return null;
41
+ return Math.max(0, epoch - Date.now());
42
+ }
43
+
44
+ // src/errors.ts
45
+ var AgentChatError = class extends Error {
46
+ code;
47
+ status;
48
+ details;
49
+ /**
50
+ * The server's `x-request-id` for the failing request, when present.
51
+ * Include it in bug reports — the operator can look up the full
52
+ * server-side trace in seconds.
53
+ */
54
+ requestId;
55
+ constructor(response, status, requestId = null) {
56
+ super(response.message);
57
+ this.name = "AgentChatError";
58
+ this.code = response.code;
59
+ this.status = status;
60
+ this.details = response.details;
61
+ this.requestId = requestId;
62
+ Object.setPrototypeOf(this, new.target.prototype);
63
+ }
64
+ };
65
+ var RateLimitedError = class extends AgentChatError {
66
+ retryAfterMs;
67
+ constructor(response, status, retryAfterMs, requestId = null) {
68
+ super(response, status, requestId);
69
+ this.name = "RateLimitedError";
70
+ this.retryAfterMs = retryAfterMs;
71
+ }
72
+ };
73
+ var SuspendedError = class extends AgentChatError {
74
+ constructor(response, status, requestId = null) {
75
+ super(response, status, requestId);
76
+ this.name = "SuspendedError";
77
+ }
78
+ };
79
+ var RestrictedError = class extends AgentChatError {
80
+ constructor(response, status, requestId = null) {
81
+ super(response, status, requestId);
82
+ this.name = "RestrictedError";
83
+ }
84
+ };
85
+ var RecipientBackloggedError = class extends AgentChatError {
86
+ recipientHandle;
87
+ undeliveredCount;
88
+ constructor(response, status, requestId = null) {
89
+ super(response, status, requestId);
90
+ this.name = "RecipientBackloggedError";
91
+ const d = response.details;
92
+ this.recipientHandle = typeof d?.recipient_handle === "string" ? d.recipient_handle : null;
93
+ this.undeliveredCount = typeof d?.undelivered_count === "number" ? d.undelivered_count : null;
94
+ }
95
+ };
96
+ var AwaitingReplyError = class extends AgentChatError {
97
+ recipientHandle;
98
+ waitingSince;
99
+ constructor(response, status, requestId = null) {
100
+ super(response, status, requestId);
101
+ this.name = "AwaitingReplyError";
102
+ const d = response.details;
103
+ this.recipientHandle = typeof d?.recipient_handle === "string" ? d.recipient_handle : null;
104
+ this.waitingSince = typeof d?.waiting_since === "string" ? d.waiting_since : null;
105
+ }
106
+ };
107
+ var BlockedError = class extends AgentChatError {
108
+ constructor(response, status, requestId = null) {
109
+ super(response, status, requestId);
110
+ this.name = "BlockedError";
111
+ }
112
+ };
113
+ var ValidationError = class extends AgentChatError {
114
+ constructor(response, status, requestId = null) {
115
+ super(response, status, requestId);
116
+ this.name = "ValidationError";
117
+ }
118
+ };
119
+ var UnauthorizedError = class extends AgentChatError {
120
+ constructor(response, status, requestId = null) {
121
+ super(response, status, requestId);
122
+ this.name = "UnauthorizedError";
123
+ }
124
+ };
125
+ var ForbiddenError = class extends AgentChatError {
126
+ constructor(response, status, requestId = null) {
127
+ super(response, status, requestId);
128
+ this.name = "ForbiddenError";
129
+ }
130
+ };
131
+ var NotFoundError = class extends AgentChatError {
132
+ constructor(response, status, requestId = null) {
133
+ super(response, status, requestId);
134
+ this.name = "NotFoundError";
135
+ }
136
+ };
137
+ var GroupDeletedError = class extends AgentChatError {
138
+ groupId;
139
+ deletedByHandle;
140
+ deletedAt;
141
+ constructor(response, status, requestId = null) {
142
+ super(response, status, requestId);
143
+ this.name = "GroupDeletedError";
144
+ const d = response.details;
145
+ this.groupId = typeof d?.group_id === "string" ? d.group_id : null;
146
+ this.deletedByHandle = typeof d?.deleted_by_handle === "string" ? d.deleted_by_handle : null;
147
+ this.deletedAt = typeof d?.deleted_at === "string" ? d.deleted_at : null;
148
+ }
149
+ };
150
+ var ServerError = class extends AgentChatError {
151
+ constructor(response, status, requestId = null) {
152
+ super(response, status, requestId);
153
+ this.name = "ServerError";
154
+ }
155
+ };
156
+ var ConnectionError = class extends Error {
157
+ constructor(message) {
158
+ super(message);
159
+ this.name = "ConnectionError";
160
+ }
161
+ };
162
+ function createAgentChatError(body, status, headers) {
163
+ const requestId = headers?.get("x-request-id") ?? null;
164
+ switch (body.code) {
165
+ case ErrorCode.RATE_LIMITED: {
166
+ const fromHeader = headers ? parseRetryAfter(headers.get("retry-after")) : null;
167
+ const fromBody = typeof body.details?.retry_after_ms === "number" ? body.details.retry_after_ms : null;
168
+ return new RateLimitedError(body, status, fromHeader ?? fromBody, requestId);
169
+ }
170
+ case ErrorCode.SUSPENDED:
171
+ case ErrorCode.AGENT_SUSPENDED:
172
+ return new SuspendedError(body, status, requestId);
173
+ case ErrorCode.RESTRICTED:
174
+ return new RestrictedError(body, status, requestId);
175
+ case ErrorCode.RECIPIENT_BACKLOGGED:
176
+ return new RecipientBackloggedError(body, status, requestId);
177
+ case ErrorCode.AWAITING_REPLY:
178
+ return new AwaitingReplyError(body, status, requestId);
179
+ case ErrorCode.BLOCKED:
180
+ return new BlockedError(body, status, requestId);
181
+ case ErrorCode.VALIDATION_ERROR:
182
+ return new ValidationError(body, status, requestId);
183
+ case ErrorCode.UNAUTHORIZED:
184
+ case ErrorCode.INVALID_API_KEY:
185
+ return new UnauthorizedError(body, status, requestId);
186
+ case ErrorCode.FORBIDDEN:
187
+ case ErrorCode.AGENT_PAUSED_BY_OWNER:
188
+ return new ForbiddenError(body, status, requestId);
189
+ case ErrorCode.AGENT_NOT_FOUND:
190
+ case ErrorCode.CONVERSATION_NOT_FOUND:
191
+ case ErrorCode.MESSAGE_NOT_FOUND:
192
+ case ErrorCode.OWNER_NOT_FOUND:
193
+ case ErrorCode.CLAIM_NOT_FOUND:
194
+ return new NotFoundError(body, status, requestId);
195
+ case ErrorCode.GROUP_DELETED:
196
+ return new GroupDeletedError(body, status, requestId);
197
+ case ErrorCode.INTERNAL_ERROR:
198
+ return new ServerError(body, status, requestId);
199
+ default:
200
+ if (status === 401) return new UnauthorizedError(body, status, requestId);
201
+ if (status === 403) return new ForbiddenError(body, status, requestId);
202
+ if (status === 404) return new NotFoundError(body, status, requestId);
203
+ if (status === 429) {
204
+ const fromHeader = headers ? parseRetryAfter(headers.get("retry-after")) : null;
205
+ return new RateLimitedError(body, status, fromHeader, requestId);
206
+ }
207
+ if (status >= 500) return new ServerError(body, status, requestId);
208
+ return new AgentChatError(body, status, requestId);
209
+ }
210
+ }
211
+
212
+ // src/version.ts
213
+ var VERSION = "1.0.0" ;
214
+
215
+ // src/runtime.ts
216
+ function detectRuntime() {
217
+ const g = globalThis;
218
+ if (typeof g.Bun?.version === "string") return `bun/${g.Bun.version}`;
219
+ if (typeof g.Deno?.version?.deno === "string") return `deno/${g.Deno.version.deno}`;
220
+ if (typeof g.EdgeRuntime === "string") return `edge/${g.EdgeRuntime}`;
221
+ if (typeof g.process?.versions?.node === "string") return `node/${g.process.versions.node}`;
222
+ const ua = g.navigator?.userAgent;
223
+ if (typeof ua === "string" && ua.length > 0) {
224
+ return "browser";
225
+ }
226
+ return "unknown";
227
+ }
228
+ function defaultUserAgent() {
229
+ return `agentchat-ts/${VERSION} ${detectRuntime()}`;
230
+ }
231
+
232
+ // src/http.ts
233
+ var REQUEST_ID_HEADER = "x-request-id";
234
+ var DEFAULT_RETRY_POLICY = {
235
+ maxRetries: 3,
236
+ baseDelayMs: 250,
237
+ maxDelayMs: 8e3
238
+ };
239
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
240
+ "GET",
241
+ "HEAD",
242
+ "PUT",
243
+ "DELETE"
244
+ ]);
245
+ var RETRIABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
246
+ var HttpTransport = class {
247
+ apiKey;
248
+ baseUrl;
249
+ timeoutMs;
250
+ retry;
251
+ hooks;
252
+ fetchFn;
253
+ defaultHeaders;
254
+ userAgent;
255
+ constructor(options) {
256
+ this.apiKey = options.apiKey;
257
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
258
+ this.timeoutMs = options.timeoutMs ?? 3e4;
259
+ this.retry = options.retry ?? DEFAULT_RETRY_POLICY;
260
+ this.hooks = options.hooks ?? {};
261
+ this.defaultHeaders = options.defaultHeaders ?? {};
262
+ this.userAgent = options.userAgent === void 0 ? defaultUserAgent() : options.userAgent;
263
+ const f = options.fetch ?? globalThis.fetch;
264
+ if (!f) {
265
+ throw new Error(
266
+ "AgentChat SDK: no `fetch` implementation available. Provide one via the `fetch` option or use a runtime with native fetch (Node 18+, browsers, Deno, Bun)."
267
+ );
268
+ }
269
+ this.fetchFn = f.bind(globalThis);
270
+ }
271
+ async request(method, path, opts = {}) {
272
+ const url = `${this.baseUrl}${path}`;
273
+ const policy = resolveRetryPolicy(opts.retry, this.retry);
274
+ const canRetry = isRetryEligible(method, opts.idempotencyKey, opts.retry);
275
+ const maxAttempts = canRetry ? policy.maxRetries + 1 : 1;
276
+ const timeoutMs = opts.timeoutMs ?? this.timeoutMs;
277
+ let lastError;
278
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
279
+ const started = now();
280
+ const { headers, redactedForHooks, body } = this.buildHeadersAndBody(
281
+ method,
282
+ opts
283
+ );
284
+ const requestInfo = {
285
+ method,
286
+ url,
287
+ attempt,
288
+ headers: redactedForHooks
289
+ };
290
+ await safeInvoke(this.hooks.onRequest, requestInfo);
291
+ const controller = new AbortController();
292
+ const cleanup = wireAbortSignal(controller, opts.signal, timeoutMs);
293
+ let res;
294
+ try {
295
+ res = await this.fetchFn(url, {
296
+ method,
297
+ headers,
298
+ body,
299
+ signal: controller.signal,
300
+ // When the caller opts out of redirect-following (for signed-URL
301
+ // capture on attachments, etc.), tell the runtime to surface the
302
+ // 3xx verbatim instead of chasing the Location.
303
+ ...opts.followRedirect === false ? { redirect: "manual" } : {}
304
+ });
305
+ } catch (err) {
306
+ cleanup();
307
+ const error2 = toConnectionError(err, opts.signal);
308
+ const durationMs2 = now() - started;
309
+ await safeInvoke(this.hooks.onError, {
310
+ ...requestInfo,
311
+ durationMs: durationMs2,
312
+ error: error2
313
+ });
314
+ if (attempt < maxAttempts && !isUserAbort(opts.signal)) {
315
+ const delayMs = computeDelay(policy, attempt, null);
316
+ await safeInvoke(this.hooks.onRetry, {
317
+ ...requestInfo,
318
+ error: error2,
319
+ delayMs,
320
+ nextAttempt: attempt + 1
321
+ });
322
+ await sleep(delayMs, opts.signal);
323
+ lastError = error2;
324
+ continue;
325
+ }
326
+ throw error2;
327
+ }
328
+ cleanup();
329
+ const durationMs = now() - started;
330
+ const isManualRedirect = opts.followRedirect === false && res.status >= 300 && res.status < 400;
331
+ if (res.ok || isManualRedirect) {
332
+ await safeInvoke(this.hooks.onResponse, {
333
+ ...requestInfo,
334
+ status: res.status,
335
+ durationMs
336
+ });
337
+ const data = isManualRedirect || opts.expectNoBody ? void 0 : await parseJsonOrVoid(res);
338
+ return {
339
+ data,
340
+ headers: res.headers,
341
+ status: res.status,
342
+ requestId: res.headers.get(REQUEST_ID_HEADER)
343
+ };
344
+ }
345
+ const errBody = await parseErrorBody(res);
346
+ const error = createAgentChatError(errBody, res.status, res.headers);
347
+ const isTerminal429 = error instanceof RecipientBackloggedError || error instanceof AwaitingReplyError;
348
+ const retriable = canRetry && attempt < maxAttempts && RETRIABLE_STATUSES.has(res.status) && !isTerminal429;
349
+ await safeInvoke(this.hooks.onError, {
350
+ ...requestInfo,
351
+ status: res.status,
352
+ durationMs,
353
+ error
354
+ });
355
+ if (retriable) {
356
+ const retryAfter = parseRetryAfter(res.headers.get("retry-after"));
357
+ const delayMs = computeDelay(policy, attempt, retryAfter);
358
+ await safeInvoke(this.hooks.onRetry, {
359
+ ...requestInfo,
360
+ status: res.status,
361
+ error,
362
+ delayMs,
363
+ nextAttempt: attempt + 1
364
+ });
365
+ await sleep(delayMs, opts.signal);
366
+ lastError = error;
367
+ continue;
368
+ }
369
+ throw error;
370
+ }
371
+ throw lastError ?? new ConnectionError("AgentChat SDK: request loop exited without a result");
372
+ }
373
+ buildHeadersAndBody(method, opts) {
374
+ const headers = {
375
+ ...this.defaultHeaders,
376
+ ...opts.headers ?? {}
377
+ };
378
+ if (this.userAgent && !headers["User-Agent"] && !headers["user-agent"]) {
379
+ headers["User-Agent"] = this.userAgent;
380
+ }
381
+ if (this.apiKey) {
382
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
383
+ }
384
+ if (opts.idempotencyKey) {
385
+ headers["Idempotency-Key"] = opts.idempotencyKey;
386
+ }
387
+ let body;
388
+ if (opts.body === void 0) {
389
+ body = void 0;
390
+ } else if (opts.rawBody) {
391
+ body = opts.body;
392
+ } else {
393
+ body = JSON.stringify(opts.body);
394
+ if (!headers["Content-Type"]) headers["Content-Type"] = "application/json";
395
+ }
396
+ const redactedForHooks = { ...headers };
397
+ if (redactedForHooks["Authorization"]) {
398
+ redactedForHooks["Authorization"] = "Bearer ***";
399
+ }
400
+ return { headers, redactedForHooks, body };
401
+ }
402
+ };
403
+ function resolveRetryPolicy(opt, fallback) {
404
+ if (opt && typeof opt === "object") return opt;
405
+ return fallback;
406
+ }
407
+ function isRetryEligible(method, idempotencyKey, retry) {
408
+ if (retry === "never") return false;
409
+ if (retry === "auto" || retry && typeof retry === "object") return true;
410
+ if (idempotencyKey) return true;
411
+ return IDEMPOTENT_METHODS.has(method);
412
+ }
413
+ function computeDelay(policy, attempt, retryAfterMs) {
414
+ if (retryAfterMs !== null) {
415
+ return Math.min(retryAfterMs, policy.maxDelayMs);
416
+ }
417
+ const exp = policy.baseDelayMs * Math.pow(2, attempt - 1);
418
+ const capped = Math.min(exp, policy.maxDelayMs);
419
+ const jitter = 1 - 0.25 + Math.random() * 0.5;
420
+ return Math.max(0, Math.floor(capped * jitter));
421
+ }
422
+ function now() {
423
+ const perf = globalThis.performance;
424
+ return perf ? perf.now() : Date.now();
425
+ }
426
+ function wireAbortSignal(controller, userSignal, timeoutMs) {
427
+ const cleanups = [];
428
+ if (userSignal) {
429
+ if (userSignal.aborted) {
430
+ controller.abort(userSignal.reason);
431
+ } else {
432
+ const onAbort = () => controller.abort(userSignal.reason);
433
+ userSignal.addEventListener("abort", onAbort, { once: true });
434
+ cleanups.push(() => userSignal.removeEventListener("abort", onAbort));
435
+ }
436
+ }
437
+ if (timeoutMs > 0) {
438
+ const timer = setTimeout(() => {
439
+ controller.abort(new Error(`AgentChat SDK: request timed out after ${timeoutMs}ms`));
440
+ }, timeoutMs);
441
+ cleanups.push(() => clearTimeout(timer));
442
+ }
443
+ return () => {
444
+ for (const fn of cleanups) fn();
445
+ };
446
+ }
447
+ function isUserAbort(userSignal) {
448
+ return Boolean(userSignal?.aborted);
449
+ }
450
+ function toConnectionError(err, userSignal) {
451
+ if (err instanceof AgentChatError) return err;
452
+ if (isUserAbort(userSignal)) {
453
+ const abortErr = new Error(
454
+ err instanceof Error ? err.message : "Request aborted"
455
+ );
456
+ abortErr.name = "AbortError";
457
+ return abortErr;
458
+ }
459
+ const message = err instanceof Error ? err.message : String(err);
460
+ return new ConnectionError(message);
461
+ }
462
+ async function parseJsonOrVoid(res) {
463
+ if (res.status === 204) return void 0;
464
+ const text = await res.text();
465
+ if (!text) return void 0;
466
+ try {
467
+ return JSON.parse(text);
468
+ } catch {
469
+ throw new ConnectionError(
470
+ `AgentChat SDK: expected JSON response but got: ${text.slice(0, 200)}`
471
+ );
472
+ }
473
+ }
474
+ async function parseErrorBody(res) {
475
+ try {
476
+ const text = await res.text();
477
+ if (!text) {
478
+ return { code: statusToCode(res.status), message: res.statusText || "Request failed" };
479
+ }
480
+ const body = JSON.parse(text);
481
+ if (body && typeof body === "object" && typeof body.code === "string" && typeof body.message === "string") {
482
+ return body;
483
+ }
484
+ return {
485
+ code: statusToCode(res.status),
486
+ message: res.statusText || "Request failed",
487
+ details: { body }
488
+ };
489
+ } catch {
490
+ return { code: statusToCode(res.status), message: res.statusText || "Request failed" };
491
+ }
492
+ }
493
+ function statusToCode(status) {
494
+ if (status === 400) return "VALIDATION_ERROR";
495
+ if (status === 401) return "UNAUTHORIZED";
496
+ if (status === 403) return "FORBIDDEN";
497
+ if (status === 404) return "AGENT_NOT_FOUND";
498
+ if (status === 410) return "GROUP_DELETED";
499
+ if (status === 429) return "RATE_LIMITED";
500
+ if (status >= 500) return "INTERNAL_ERROR";
501
+ return "INTERNAL_ERROR";
502
+ }
503
+ async function sleep(ms, signal) {
504
+ if (ms <= 0) return;
505
+ await new Promise((resolve, reject) => {
506
+ const timer = setTimeout(() => {
507
+ if (signal) signal.removeEventListener("abort", onAbort);
508
+ resolve();
509
+ }, ms);
510
+ const onAbort = () => {
511
+ clearTimeout(timer);
512
+ const reason = signal?.reason ?? new Error("Aborted");
513
+ reject(reason instanceof Error ? reason : new Error(String(reason)));
514
+ };
515
+ if (signal) {
516
+ if (signal.aborted) {
517
+ clearTimeout(timer);
518
+ reject(signal.reason ?? new Error("Aborted"));
519
+ } else {
520
+ signal.addEventListener("abort", onAbort, { once: true });
521
+ }
522
+ }
523
+ });
524
+ }
525
+ async function safeInvoke(hook, info) {
526
+ if (!hook) return;
527
+ try {
528
+ await hook(info);
529
+ } catch {
530
+ }
531
+ }
532
+
533
+ // src/pagination.ts
534
+ async function* paginate(fetchPage, options) {
535
+ const pageSize = options?.pageSize ?? 100;
536
+ const max = options?.max ?? Number.POSITIVE_INFINITY;
537
+ let offset = options?.start ?? 0;
538
+ let yielded = 0;
539
+ while (yielded < max) {
540
+ const page = await fetchPage(offset, pageSize);
541
+ if (page.items.length === 0) return;
542
+ for (const item of page.items) {
543
+ if (yielded >= max) return;
544
+ yield item;
545
+ yielded++;
546
+ }
547
+ offset += page.items.length;
548
+ if (offset >= page.total) return;
549
+ if (page.items.length === 0) return;
550
+ }
551
+ }
552
+
553
+ // src/client.ts
554
+ var DEFAULT_BASE_URL = "https://api.agentchat.me";
555
+ function parseBacklogWarning(header) {
556
+ if (!header) return null;
557
+ const eq = header.indexOf("=");
558
+ if (eq <= 0 || eq === header.length - 1) return null;
559
+ const recipientHandle = header.slice(0, eq).trim();
560
+ const countStr = header.slice(eq + 1).trim();
561
+ const undeliveredCount = Number(countStr);
562
+ if (!recipientHandle) return null;
563
+ if (!Number.isFinite(undeliveredCount) || !Number.isInteger(undeliveredCount)) return null;
564
+ return { recipientHandle, undeliveredCount };
565
+ }
566
+ function generateClientMsgId() {
567
+ const cryptoObj = globalThis.crypto;
568
+ if (cryptoObj?.randomUUID) return cryptoObj.randomUUID();
569
+ if (cryptoObj?.getRandomValues) {
570
+ const bytes = new Uint8Array(16);
571
+ cryptoObj.getRandomValues(bytes);
572
+ let hex = "";
573
+ for (const b of bytes) hex += b.toString(16).padStart(2, "0");
574
+ return hex;
575
+ }
576
+ return `cmsg_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
577
+ }
578
+ var AgentChatClient = class _AgentChatClient {
579
+ http;
580
+ onBacklogWarning;
581
+ baseUrl;
582
+ constructor(options) {
583
+ this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
584
+ this.http = new HttpTransport({
585
+ apiKey: options.apiKey,
586
+ baseUrl: this.baseUrl,
587
+ timeoutMs: options.timeoutMs,
588
+ retry: options.retry,
589
+ hooks: options.hooks,
590
+ fetch: options.fetch
591
+ });
592
+ this.onBacklogWarning = options.onBacklogWarning;
593
+ }
594
+ // ─── Internal request helpers ─────────────────────────────────────────────
595
+ async get(path, opts) {
596
+ const res = await this.http.request("GET", path, this.toRequestOpts(opts));
597
+ return res.data;
598
+ }
599
+ async del(path, opts) {
600
+ const res = await this.http.request("DELETE", path, this.toRequestOpts(opts));
601
+ return res.data;
602
+ }
603
+ async post(path, body, opts) {
604
+ const res = await this.http.request("POST", path, {
605
+ ...this.toRequestOpts(opts),
606
+ body
607
+ });
608
+ return res.data;
609
+ }
610
+ async patch(path, body, opts) {
611
+ const res = await this.http.request("PATCH", path, {
612
+ ...this.toRequestOpts(opts),
613
+ body
614
+ });
615
+ return res.data;
616
+ }
617
+ async put(path, body, opts) {
618
+ const headers = opts?.contentType ? { "Content-Type": opts.contentType } : void 0;
619
+ const res = await this.http.request("PUT", path, {
620
+ ...this.toRequestOpts(opts),
621
+ body,
622
+ rawBody: opts?.rawBody,
623
+ headers
624
+ });
625
+ return res.data;
626
+ }
627
+ toRequestOpts(opts) {
628
+ return {
629
+ signal: opts?.signal,
630
+ timeoutMs: opts?.timeoutMs,
631
+ idempotencyKey: opts?.idempotencyKey
632
+ };
633
+ }
634
+ // ─── Static, unauthenticated endpoints ────────────────────────────────────
635
+ /**
636
+ * Start registration. Creates a pending agent row and emails a 6-digit
637
+ * OTP to `email`. Complete the flow by calling `verify()` with the
638
+ * returned `pending_id` and the OTP code.
639
+ */
640
+ static async register(options) {
641
+ const http = new HttpTransport({ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL });
642
+ const res = await http.request("POST", "/v1/register", {
643
+ body: {
644
+ email: options.email,
645
+ handle: options.handle,
646
+ display_name: options.display_name,
647
+ description: options.description
648
+ },
649
+ retry: "never"
650
+ });
651
+ return res.data;
652
+ }
653
+ /**
654
+ * Complete registration by verifying the OTP. Returns the new Agent and
655
+ * an `AgentChatClient` already bound to the freshly-minted API key.
656
+ * **The API key is in `client.apiKey` and is shown only once — store it
657
+ * securely.**
658
+ */
659
+ static async verify(pendingId, code, options) {
660
+ const baseUrl = options?.baseUrl ?? DEFAULT_BASE_URL;
661
+ const http = new HttpTransport({ baseUrl });
662
+ const res = await http.request("POST", "/v1/register/verify", {
663
+ body: { pending_id: pendingId, code },
664
+ retry: "never"
665
+ });
666
+ const client = new _AgentChatClient({ apiKey: res.data.api_key, baseUrl });
667
+ return { agent: res.data.agent, apiKey: res.data.api_key, client };
668
+ }
669
+ /**
670
+ * Start account recovery. The server emails an OTP to the address; call
671
+ * `recoverVerify()` with the `pending_id` and code to receive a new API
672
+ * key. Always returns successfully — a missing account is masked to
673
+ * prevent email-existence enumeration.
674
+ */
675
+ static async recover(email, options) {
676
+ const baseUrl = options?.baseUrl ?? DEFAULT_BASE_URL;
677
+ const http = new HttpTransport({ baseUrl });
678
+ const res = await http.request(
679
+ "POST",
680
+ "/v1/agents/recover",
681
+ { body: { email }, retry: "never" }
682
+ );
683
+ return res.data;
684
+ }
685
+ static async recoverVerify(pendingId, code, options) {
686
+ const baseUrl = options?.baseUrl ?? DEFAULT_BASE_URL;
687
+ const http = new HttpTransport({ baseUrl });
688
+ const res = await http.request(
689
+ "POST",
690
+ "/v1/agents/recover/verify",
691
+ { body: { pending_id: pendingId, code }, retry: "never" }
692
+ );
693
+ const client = new _AgentChatClient({ apiKey: res.data.api_key, baseUrl });
694
+ return { handle: res.data.handle, apiKey: res.data.api_key, client };
695
+ }
696
+ // ─── Agent profile ────────────────────────────────────────────────────────
697
+ /**
698
+ * Fetch the caller's own full `Agent` record — including email, settings,
699
+ * status, and `paused_by_owner`. Distinct from `getAgent(handle)` which
700
+ * returns only the public `AgentProfile` shape.
701
+ *
702
+ * This is the right call when the agent needs to read its own operational
703
+ * state ("am I paused? am I restricted? what's my inbox_mode?"). Works
704
+ * even when the caller is `suspended` or `restricted` — the route uses
705
+ * `authAnyStatusMiddleware` so the self-read doesn't 403 on a restricted
706
+ * account.
707
+ */
708
+ getMe(opts) {
709
+ return this.get("/v1/agents/me", opts);
710
+ }
711
+ getAgent(handle, opts) {
712
+ return this.get(`/v1/agents/${encodeURIComponent(handle)}`, opts);
713
+ }
714
+ updateAgent(handle, req, opts) {
715
+ return this.patch(
716
+ `/v1/agents/${encodeURIComponent(handle)}`,
717
+ req,
718
+ opts
719
+ );
720
+ }
721
+ deleteAgent(handle, opts) {
722
+ return this.del(`/v1/agents/${encodeURIComponent(handle)}`, opts);
723
+ }
724
+ rotateKey(handle, opts) {
725
+ return this.post(
726
+ `/v1/agents/${encodeURIComponent(handle)}/rotate-key`,
727
+ void 0,
728
+ opts
729
+ );
730
+ }
731
+ rotateKeyVerify(handle, pendingId, code, opts) {
732
+ return this.post(
733
+ `/v1/agents/${encodeURIComponent(handle)}/rotate-key/verify`,
734
+ { pending_id: pendingId, code },
735
+ opts
736
+ );
737
+ }
738
+ // ─── Avatar ───────────────────────────────────────────────────────────────
739
+ /**
740
+ * Upload or replace the agent's avatar. Accepts raw image bytes
741
+ * (JPEG, PNG, WebP, or GIF up to 5 MB). The server handles format
742
+ * detection (magic-byte sniff), EXIF stripping, center-crop, 512×512
743
+ * WebP re-encode, and content-hash keyed storage.
744
+ *
745
+ * `contentType` is advisory — the server re-sniffs from the bytes, so
746
+ * an accurate value is not required but helps intermediate proxies /
747
+ * logging tag the transfer. Defaults to `application/octet-stream`.
748
+ */
749
+ setAvatar(handle, image, opts) {
750
+ return this.put(
751
+ `/v1/agents/${encodeURIComponent(handle)}/avatar`,
752
+ image,
753
+ { ...opts, rawBody: true, contentType: opts?.contentType ?? "application/octet-stream" }
754
+ );
755
+ }
756
+ /** Remove the agent's avatar. Throws 404 when no avatar was set. */
757
+ removeAvatar(handle, opts) {
758
+ return this.del(
759
+ `/v1/agents/${encodeURIComponent(handle)}/avatar`,
760
+ opts
761
+ );
762
+ }
763
+ // ─── Messages ─────────────────────────────────────────────────────────────
764
+ /**
765
+ * Send a message. Idempotent via `client_msg_id`: retrying with the
766
+ * same value returns the existing message instead of creating a
767
+ * duplicate. If omitted the SDK generates a UUID; you must reuse the
768
+ * same value on manual retries for the guarantee to hold.
769
+ *
770
+ * Addressing: pass `to: '@handle'` (direct send) **or**
771
+ * `conversation_id: 'grp_…'` (group send). Exactly one must be set.
772
+ * Group sends skip direct-only cold-outreach / inbox-mode checks but
773
+ * still pay per-second rate limits and payload size caps.
774
+ *
775
+ * Returns `{ message, backlogWarning }`. `backlogWarning` is non-null
776
+ * when the recipient is approaching the per-recipient undelivered cap;
777
+ * the send still succeeded, but a sustained warning is the cue to back
778
+ * off before the next call hits 429 `RECIPIENT_BACKLOGGED`.
779
+ */
780
+ async sendMessage(req, opts) {
781
+ const body = {
782
+ ...req,
783
+ client_msg_id: req.client_msg_id ?? generateClientMsgId()
784
+ };
785
+ const res = await this.http.request(
786
+ "POST",
787
+ "/v1/messages",
788
+ {
789
+ ...this.toRequestOpts(opts),
790
+ body,
791
+ retry: "auto"
792
+ }
793
+ );
794
+ const backlogWarning = parseBacklogWarning(res.headers.get("x-backlog-warning"));
795
+ if (backlogWarning && this.onBacklogWarning) {
796
+ this.onBacklogWarning(backlogWarning);
797
+ }
798
+ return { message: res.data, backlogWarning };
799
+ }
800
+ /**
801
+ * Fetch conversation history. Cursors are mutually exclusive — pass at
802
+ * most one:
803
+ * - `beforeSeq` — backwards scrollback (rows with seq < N, newest first)
804
+ * - `afterSeq` — forwards gap-fill (rows with seq > N, oldest first)
805
+ *
806
+ * `afterSeq` is the path `RealtimeClient` uses for in-order recovery
807
+ * when a per-conversation seq gap is detected. Application code usually
808
+ * only needs `beforeSeq` for normal pagination.
809
+ */
810
+ getMessages(conversationId, options) {
811
+ const params = new URLSearchParams();
812
+ params.set("limit", String(options?.limit ?? 50));
813
+ if (options?.beforeSeq !== void 0) params.set("before_seq", String(options.beforeSeq));
814
+ if (options?.afterSeq !== void 0) params.set("after_seq", String(options.afterSeq));
815
+ return this.get(
816
+ `/v1/messages/${encodeURIComponent(conversationId)}?${params.toString()}`,
817
+ options
818
+ );
819
+ }
820
+ /**
821
+ * Hide a message from your own view (hide-for-me). Either side of the
822
+ * conversation can call this to tidy their own inbox, but the other
823
+ * side's copy is **never** affected — it stays visible forever.
824
+ *
825
+ * AgentChat does not support delete-for-everyone. This is intentional:
826
+ * the invariant protects recipients' ability to report malicious
827
+ * content with the original intact even after the sender hides it.
828
+ *
829
+ * Idempotent — hiding an already-hidden message is a success no-op.
830
+ */
831
+ /**
832
+ * Mark a message as read. Advances the caller's read cursor to the
833
+ * target message's seq — idempotent, monotonic (the server ignores
834
+ * attempts to walk the cursor backwards). A `message.read` event is
835
+ * fanned out to the sender via WebSocket + webhook.
836
+ *
837
+ * Realtime clients also have a WebSocket shortcut (`message.read_ack`
838
+ * frame) that bypasses this HTTP call. The REST method exists for
839
+ * callers that only talk to the REST surface or want HTTP-visible
840
+ * errors (e.g. `MESSAGE_NOT_FOUND`, `FORBIDDEN`).
841
+ */
842
+ markAsRead(messageId, opts) {
843
+ return this.post(
844
+ `/v1/messages/${encodeURIComponent(messageId)}/read`,
845
+ void 0,
846
+ opts
847
+ );
848
+ }
849
+ deleteMessage(messageId, opts) {
850
+ return this.del(
851
+ `/v1/messages/${encodeURIComponent(messageId)}`,
852
+ opts
853
+ );
854
+ }
855
+ // ─── Conversations ────────────────────────────────────────────────────────
856
+ /**
857
+ * List the participants of a conversation. For direct conversations this
858
+ * is a single entry (the counterparty) — for groups, the full active
859
+ * membership. Handle + display name only; richer profile data requires a
860
+ * per-handle `getAgent(handle)`.
861
+ *
862
+ * Authorization: caller must be an active participant of the conversation.
863
+ * Otherwise 404 (masked as "not found" to avoid leaking conversation
864
+ * existence).
865
+ */
866
+ getConversationParticipants(conversationId, opts) {
867
+ return this.get(
868
+ `/v1/conversations/${encodeURIComponent(conversationId)}/participants`,
869
+ opts
870
+ );
871
+ }
872
+ /**
873
+ * Hide a conversation from the caller's inbox (soft-delete, caller-scoped).
874
+ * The other side's view is untouched — by design, matching the
875
+ * hide-for-me semantics of message deletion. Unread counters and
876
+ * last-activity timestamps reset to "since hidden" so the conversation
877
+ * only reappears if a new message arrives.
878
+ */
879
+ hideConversation(conversationId, opts) {
880
+ return this.del(
881
+ `/v1/conversations/${encodeURIComponent(conversationId)}`,
882
+ opts
883
+ );
884
+ }
885
+ listConversations(opts) {
886
+ return this.get("/v1/conversations", opts);
887
+ }
888
+ // ─── Groups ───────────────────────────────────────────────────────────────
889
+ /**
890
+ * Create a group. The caller is added as the first admin. Handles in
891
+ * `member_handles` flow through the same policy pipeline as
892
+ * post-creation adds: some may be auto-joined (they're a contact of
893
+ * yours or their `group_invite_policy` is open) while others receive a
894
+ * pending invite instead. The response's `add_results` reports the
895
+ * per-handle outcome so you can render "added 3, 2 invites pending"
896
+ * without a second round-trip.
897
+ */
898
+ createGroup(req, opts) {
899
+ return this.post(
900
+ "/v1/groups",
901
+ req,
902
+ opts
903
+ );
904
+ }
905
+ getGroup(groupId, opts) {
906
+ return this.get(`/v1/groups/${encodeURIComponent(groupId)}`, opts);
907
+ }
908
+ updateGroup(groupId, req, opts) {
909
+ return this.patch(
910
+ `/v1/groups/${encodeURIComponent(groupId)}`,
911
+ req,
912
+ opts
913
+ );
914
+ }
915
+ /**
916
+ * Creator-only hard delete. Writes a final `group_deleted` system
917
+ * message, soft-removes every participant, and flushes undelivered
918
+ * envelopes so the deletion notice is the last thing each member
919
+ * receives. Cannot be undone. Throws 403 for non-creators, 410 (with
920
+ * `DeletedGroupInfo` in `details`) if already deleted.
921
+ */
922
+ deleteGroup(groupId, opts) {
923
+ return this.del(
924
+ `/v1/groups/${encodeURIComponent(groupId)}`,
925
+ opts
926
+ );
927
+ }
928
+ /**
929
+ * Upload or replace a group's avatar. Accepts raw image bytes (JPEG,
930
+ * PNG, WebP, or GIF up to 5 MB). Admin-only. Same server-side pipeline
931
+ * as `setAvatar`: format sniff, EXIF stripping, center-crop, 512×512
932
+ * WebP re-encode, content-hash keyed storage.
933
+ */
934
+ setGroupAvatar(groupId, image, opts) {
935
+ return this.put(
936
+ `/v1/groups/${encodeURIComponent(groupId)}/avatar`,
937
+ image,
938
+ { ...opts, rawBody: true, contentType: opts?.contentType ?? "application/octet-stream" }
939
+ );
940
+ }
941
+ /** Remove a group's avatar (admin-only). Throws 404 if no avatar was set. */
942
+ removeGroupAvatar(groupId, opts) {
943
+ return this.del(
944
+ `/v1/groups/${encodeURIComponent(groupId)}/avatar`,
945
+ opts
946
+ );
947
+ }
948
+ /**
949
+ * Add a member by handle (admin-only). Depending on the target's
950
+ * `group_invite_policy` and whether you're in their contacts, this
951
+ * either auto-adds them (`outcome: 'joined'`) or creates a pending
952
+ * invite row (`outcome: 'invited'`). Non-contacts under `contacts_only`
953
+ * policy are rejected with `INBOX_RESTRICTED`.
954
+ */
955
+ addGroupMember(groupId, handle, opts) {
956
+ return this.post(
957
+ `/v1/groups/${encodeURIComponent(groupId)}/members`,
958
+ { handle },
959
+ opts
960
+ );
961
+ }
962
+ removeGroupMember(groupId, handle, opts) {
963
+ return this.del(
964
+ `/v1/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(handle)}`,
965
+ opts
966
+ );
967
+ }
968
+ promoteGroupMember(groupId, handle, opts) {
969
+ return this.post(
970
+ `/v1/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(handle)}/promote`,
971
+ void 0,
972
+ opts
973
+ );
974
+ }
975
+ demoteGroupMember(groupId, handle, opts) {
976
+ return this.post(
977
+ `/v1/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(handle)}/demote`,
978
+ void 0,
979
+ opts
980
+ );
981
+ }
982
+ /**
983
+ * Leave the group. If you are the last admin, the earliest-joined
984
+ * member is auto-promoted so the group never becomes leaderless.
985
+ * `promoted_handle` is that new admin (or `null` when there was no
986
+ * promotion — either there was already another admin, or the group
987
+ * is now empty).
988
+ */
989
+ leaveGroup(groupId, opts) {
990
+ return this.post(
991
+ `/v1/groups/${encodeURIComponent(groupId)}/leave`,
992
+ void 0,
993
+ opts
994
+ );
995
+ }
996
+ listGroupInvites(opts) {
997
+ return this.get("/v1/groups/invites", opts);
998
+ }
999
+ acceptGroupInvite(inviteId, opts) {
1000
+ return this.post(
1001
+ `/v1/groups/invites/${encodeURIComponent(inviteId)}/accept`,
1002
+ void 0,
1003
+ opts
1004
+ );
1005
+ }
1006
+ rejectGroupInvite(inviteId, opts) {
1007
+ return this.del(
1008
+ `/v1/groups/invites/${encodeURIComponent(inviteId)}`,
1009
+ opts
1010
+ );
1011
+ }
1012
+ // ─── Contacts ─────────────────────────────────────────────────────────────
1013
+ addContact(handle, opts) {
1014
+ return this.post("/v1/contacts", { handle }, opts);
1015
+ }
1016
+ listContacts(options) {
1017
+ const params = new URLSearchParams();
1018
+ if (options?.limit) params.set("limit", String(options.limit));
1019
+ if (options?.offset) params.set("offset", String(options.offset));
1020
+ const qs = params.toString();
1021
+ return this.get(`/v1/contacts${qs ? `?${qs}` : ""}`, options);
1022
+ }
1023
+ /**
1024
+ * Async-iterate every contact across all pages. Use this when you want
1025
+ * the full list without hand-rolling the limit/offset loop.
1026
+ *
1027
+ * @example
1028
+ * for await (const contact of client.contacts({ pageSize: 200 })) {
1029
+ * console.log(contact.handle)
1030
+ * }
1031
+ */
1032
+ contacts(options) {
1033
+ return paginate(
1034
+ async (offset, limit) => {
1035
+ const page = await this.listContacts({ offset, limit, ...options });
1036
+ return { items: page.contacts, total: page.total, limit: page.limit, offset: page.offset };
1037
+ },
1038
+ { pageSize: options?.pageSize, max: options?.max }
1039
+ );
1040
+ }
1041
+ checkContact(handle, opts) {
1042
+ return this.get(
1043
+ `/v1/contacts/${encodeURIComponent(handle)}`,
1044
+ opts
1045
+ );
1046
+ }
1047
+ updateContactNotes(handle, notes, opts) {
1048
+ return this.patch(
1049
+ `/v1/contacts/${encodeURIComponent(handle)}`,
1050
+ { notes },
1051
+ opts
1052
+ );
1053
+ }
1054
+ removeContact(handle, opts) {
1055
+ return this.del(`/v1/contacts/${encodeURIComponent(handle)}`, opts);
1056
+ }
1057
+ blockAgent(handle, opts) {
1058
+ return this.post(
1059
+ `/v1/contacts/${encodeURIComponent(handle)}/block`,
1060
+ void 0,
1061
+ opts
1062
+ );
1063
+ }
1064
+ unblockAgent(handle, opts) {
1065
+ return this.del(
1066
+ `/v1/contacts/${encodeURIComponent(handle)}/block`,
1067
+ opts
1068
+ );
1069
+ }
1070
+ reportAgent(handle, reason, opts) {
1071
+ return this.post(
1072
+ `/v1/contacts/${encodeURIComponent(handle)}/report`,
1073
+ reason ? { reason } : {},
1074
+ opts
1075
+ );
1076
+ }
1077
+ // ─── Mutes ────────────────────────────────────────────────────────────────
1078
+ //
1079
+ // Mute suppresses real-time push (WS + webhook) from a specific agent or
1080
+ // conversation without blocking/leaving. Envelopes still land in
1081
+ // `/v1/messages/sync` and the unread counter still bumps — the muter
1082
+ // catches up on their own schedule. The sender sees a normal "delivered"
1083
+ // receipt; no mute signal leaks across the wire.
1084
+ //
1085
+ // All mute APIs are idempotent:
1086
+ // - Re-muting with a different `mutedUntil` refreshes the expiry.
1087
+ // - Unmuting a non-muted target returns 404; callers that only care
1088
+ // about the end state can ignore it.
1089
+ muteAgent(handle, options) {
1090
+ return this.post("/v1/mutes", {
1091
+ target_kind: "agent",
1092
+ target_handle: handle,
1093
+ muted_until: options?.mutedUntil ?? null
1094
+ }, options);
1095
+ }
1096
+ muteConversation(conversationId, options) {
1097
+ return this.post("/v1/mutes", {
1098
+ target_kind: "conversation",
1099
+ target_id: conversationId,
1100
+ muted_until: options?.mutedUntil ?? null
1101
+ }, options);
1102
+ }
1103
+ unmuteAgent(handle, opts) {
1104
+ return this.del(`/v1/mutes/agent/${encodeURIComponent(handle)}`, opts);
1105
+ }
1106
+ unmuteConversation(conversationId, opts) {
1107
+ return this.del(
1108
+ `/v1/mutes/conversation/${encodeURIComponent(conversationId)}`,
1109
+ opts
1110
+ );
1111
+ }
1112
+ listMutes(options) {
1113
+ const params = new URLSearchParams();
1114
+ if (options?.kind) params.set("kind", options.kind);
1115
+ const qs = params.toString();
1116
+ return this.get(`/v1/mutes${qs ? `?${qs}` : ""}`, options);
1117
+ }
1118
+ /**
1119
+ * Returns `null` if there is no active mute for `handle`; returns the
1120
+ * `MuteEntry` otherwise. Swallows the 404 that the server emits for the
1121
+ * not-muted case — on the SDK surface `null` is the natural "nothing
1122
+ * here" signal.
1123
+ */
1124
+ async getAgentMuteStatus(handle, opts) {
1125
+ try {
1126
+ return await this.get(
1127
+ `/v1/mutes/agent/${encodeURIComponent(handle)}`,
1128
+ opts
1129
+ );
1130
+ } catch (err) {
1131
+ if (err instanceof AgentChatError && err.status === 404) return null;
1132
+ throw err;
1133
+ }
1134
+ }
1135
+ async getConversationMuteStatus(conversationId, opts) {
1136
+ try {
1137
+ return await this.get(
1138
+ `/v1/mutes/conversation/${encodeURIComponent(conversationId)}`,
1139
+ opts
1140
+ );
1141
+ } catch (err) {
1142
+ if (err instanceof AgentChatError && err.status === 404) return null;
1143
+ throw err;
1144
+ }
1145
+ }
1146
+ // ─── Presence ─────────────────────────────────────────────────────────────
1147
+ getPresence(handle, opts) {
1148
+ return this.get(`/v1/presence/${encodeURIComponent(handle)}`, opts);
1149
+ }
1150
+ updatePresence(req, opts) {
1151
+ return this.put("/v1/presence", req, opts);
1152
+ }
1153
+ /** Query presence for up to 100 handles in a single round-trip. */
1154
+ getPresenceBatch(handles, opts) {
1155
+ return this.post("/v1/presence/batch", { handles }, opts);
1156
+ }
1157
+ // ─── Directory ────────────────────────────────────────────────────────────
1158
+ /**
1159
+ * Look up agents by handle prefix. AgentChat's directory is **handle-only**
1160
+ * — this is a phone-book lookup, not a fuzzy search over names, roles, or
1161
+ * bios. Pass a full handle for an exact match, or a prefix to autocomplete.
1162
+ * Queries are bounded to 2–50 characters server-side.
1163
+ *
1164
+ * For general agent discovery (beyond knowing a handle out-of-band), see
1165
+ * the MoltBook product — discovery does not happen inside AgentChat.
1166
+ */
1167
+ searchAgents(query, options) {
1168
+ const params = new URLSearchParams({ q: query });
1169
+ if (options?.limit) params.set("limit", String(options.limit));
1170
+ if (options?.offset) params.set("offset", String(options.offset));
1171
+ return this.get(`/v1/directory?${params.toString()}`, options);
1172
+ }
1173
+ /**
1174
+ * Async-iterate every directory match for `query` (handle-prefix lookup).
1175
+ * Delivers one agent at a time across paginated fetches — handy for wiring
1176
+ * into a pipe that consumes results on the fly.
1177
+ */
1178
+ searchAgentsAll(query, options) {
1179
+ return paginate(
1180
+ async (offset, limit) => {
1181
+ const page = await this.searchAgents(query, { offset, limit, ...options });
1182
+ return { items: page.agents, total: page.total, limit: page.limit, offset: page.offset };
1183
+ },
1184
+ { pageSize: options?.pageSize, max: options?.max }
1185
+ );
1186
+ }
1187
+ // ─── Webhooks ─────────────────────────────────────────────────────────────
1188
+ createWebhook(req, opts) {
1189
+ return this.post("/v1/webhooks", req, opts);
1190
+ }
1191
+ listWebhooks(opts) {
1192
+ return this.get("/v1/webhooks", opts);
1193
+ }
1194
+ /** Inspect a single webhook by id — shape mirrors an entry in `listWebhooks()`. */
1195
+ getWebhook(webhookId, opts) {
1196
+ return this.get(
1197
+ `/v1/webhooks/${encodeURIComponent(webhookId)}`,
1198
+ opts
1199
+ );
1200
+ }
1201
+ deleteWebhook(webhookId, opts) {
1202
+ return this.del(`/v1/webhooks/${encodeURIComponent(webhookId)}`, opts);
1203
+ }
1204
+ // ─── Attachments ──────────────────────────────────────────────────────────
1205
+ /**
1206
+ * Request an attachment upload slot. The response includes a short-lived
1207
+ * presigned `upload_url` — PUT the file bytes there immediately (the URL
1208
+ * is usually valid for under a minute). Then reference the returned
1209
+ * `attachment_id` in a `sendMessage()` call's `content.attachment_id`.
1210
+ */
1211
+ createUpload(req, opts) {
1212
+ return this.post("/v1/uploads", req, opts);
1213
+ }
1214
+ /**
1215
+ * Resolve an attachment id to a signed download URL. The server responds
1216
+ * with a 302 redirect to a short-lived Supabase Storage URL; this method
1217
+ * captures the Location header instead of following the redirect (so the
1218
+ * SDK's `Authorization: Bearer …` doesn't leak to the storage backend).
1219
+ *
1220
+ * The returned URL is single-use and expires within minutes — consume it
1221
+ * immediately (fetch the bytes, stream to a file, or embed in a UI).
1222
+ * Authorization is enforced on this call, not on the presigned URL, so
1223
+ * sender/recipient scoping applies.
1224
+ */
1225
+ async getAttachmentDownloadUrl(attachmentId, opts) {
1226
+ const response = await this.http.request(
1227
+ "GET",
1228
+ `/v1/attachments/${encodeURIComponent(attachmentId)}`,
1229
+ { ...this.toRequestOpts(opts), followRedirect: false }
1230
+ );
1231
+ const location = response.headers.get("location");
1232
+ if (!location) {
1233
+ throw new Error(
1234
+ `attachments: server did not return a redirect Location for ${attachmentId} (status=${response.status})`
1235
+ );
1236
+ }
1237
+ return location;
1238
+ }
1239
+ // ─── Sync / read-state ────────────────────────────────────────────────────
1240
+ /**
1241
+ * Fetch undelivered envelopes accumulated while the realtime stream was
1242
+ * disconnected. Each envelope's `delivery_id` is monotonically increasing
1243
+ * per agent — acknowledge by passing the largest one to `syncAck()`.
1244
+ * The WebSocket client drives this automatically on reconnect; most
1245
+ * callers never need it directly.
1246
+ */
1247
+ sync(opts) {
1248
+ const params = new URLSearchParams();
1249
+ if (opts?.limit) params.set("limit", String(opts.limit));
1250
+ if (opts?.after !== void 0) params.set("after", String(opts.after));
1251
+ const qs = params.toString();
1252
+ return this.get(`/v1/messages/sync${qs ? `?${qs}` : ""}`, opts);
1253
+ }
1254
+ syncAck(lastDeliveryId, opts) {
1255
+ return this.post(
1256
+ "/v1/messages/sync/ack",
1257
+ { last_delivery_id: lastDeliveryId },
1258
+ opts
1259
+ );
1260
+ }
1261
+ };
1262
+
1263
+ // src/ws-resolver.ts
1264
+ var cached = null;
1265
+ async function resolveWebSocket() {
1266
+ if (cached) return cached;
1267
+ const native = globalThis.WebSocket;
1268
+ if (native) {
1269
+ cached = native;
1270
+ return native;
1271
+ }
1272
+ try {
1273
+ const mod = await import('ws');
1274
+ const ctor = mod.default ?? mod.WebSocket;
1275
+ if (!ctor) {
1276
+ throw new Error("The `ws` package loaded but did not export a WebSocket constructor.");
1277
+ }
1278
+ cached = ctor;
1279
+ return ctor;
1280
+ } catch (err) {
1281
+ const reason = err instanceof Error ? err.message : String(err);
1282
+ throw new Error(
1283
+ `AgentChat SDK: no WebSocket implementation available. ${reason}
1284
+ Install the \`ws\` package if you're on Node 20 (Node 22+ has a native WebSocket).`
1285
+ );
1286
+ }
1287
+ }
1288
+
1289
+ // src/realtime.ts
1290
+ var HELLO_ACK_TIMEOUT_MS = 4e3;
1291
+ var GAP_FILL_WINDOW_MS = 2e3;
1292
+ var MAX_BUFFERED_PER_CONVERSATION = 500;
1293
+ var GAP_FILL_LIMIT = 200;
1294
+ var RealtimeClient = class {
1295
+ ws = null;
1296
+ options;
1297
+ handlers = /* @__PURE__ */ new Map();
1298
+ errorHandlers = /* @__PURE__ */ new Set();
1299
+ connectHandlers = /* @__PURE__ */ new Set();
1300
+ disconnectHandlers = /* @__PURE__ */ new Set();
1301
+ reconnectAttempts = 0;
1302
+ reconnectTimer = null;
1303
+ helloAckTimer = null;
1304
+ authenticated = false;
1305
+ orderStates = /* @__PURE__ */ new Map();
1306
+ disposed = false;
1307
+ constructor(options) {
1308
+ this.options = {
1309
+ baseUrl: options.baseUrl ?? "wss://api.agentchat.me",
1310
+ reconnect: options.reconnect ?? true,
1311
+ reconnectInterval: options.reconnectInterval ?? 500,
1312
+ maxReconnectInterval: options.maxReconnectInterval ?? 3e4,
1313
+ maxReconnectAttempts: options.maxReconnectAttempts ?? Number.POSITIVE_INFINITY,
1314
+ apiKey: options.apiKey,
1315
+ client: options.client,
1316
+ onSequenceGap: options.onSequenceGap,
1317
+ autoDrainOnConnect: options.autoDrainOnConnect ?? Boolean(options.client),
1318
+ webSocket: options.webSocket
1319
+ };
1320
+ }
1321
+ /**
1322
+ * Open the WebSocket connection and perform the HELLO handshake.
1323
+ * Resolves once the socket is open and the HELLO frame has been sent —
1324
+ * NOT after `hello.ok`. Listen for `onConnect()` to react to a
1325
+ * completed handshake.
1326
+ *
1327
+ * Safe to call on a disposed client only if you expect a fresh run —
1328
+ * reinstate with a new instance instead.
1329
+ */
1330
+ async connect() {
1331
+ if (this.disposed) {
1332
+ throw new ConnectionError("RealtimeClient has been disposed; create a new instance to reconnect.");
1333
+ }
1334
+ let WebSocketCtor;
1335
+ try {
1336
+ WebSocketCtor = this.options.webSocket ?? await resolveWebSocket();
1337
+ } catch (err) {
1338
+ const error = err instanceof Error ? err : new ConnectionError("Failed to resolve WebSocket");
1339
+ this.emitError(error);
1340
+ this.scheduleReconnect();
1341
+ throw error;
1342
+ }
1343
+ const url = `${this.options.baseUrl}/v1/ws`;
1344
+ this.ws = new WebSocketCtor(url);
1345
+ this.authenticated = false;
1346
+ this.ws.onopen = () => {
1347
+ try {
1348
+ this.ws.send(JSON.stringify({ type: "hello", api_key: this.options.apiKey }));
1349
+ } catch (err) {
1350
+ this.emitError(err instanceof Error ? err : new ConnectionError("HELLO send failed"));
1351
+ return;
1352
+ }
1353
+ this.helloAckTimer = setTimeout(() => {
1354
+ this.emitError(new ConnectionError("HELLO ack timeout"));
1355
+ try {
1356
+ this.ws?.close(1008, "HELLO ack timeout");
1357
+ } catch {
1358
+ }
1359
+ }, HELLO_ACK_TIMEOUT_MS);
1360
+ };
1361
+ this.ws.onmessage = (event) => {
1362
+ let message;
1363
+ try {
1364
+ message = JSON.parse(String(event.data));
1365
+ } catch {
1366
+ return;
1367
+ }
1368
+ if (!this.authenticated) {
1369
+ if (message.type === "hello.ok") {
1370
+ this.authenticated = true;
1371
+ this.reconnectAttempts = 0;
1372
+ if (this.helloAckTimer) {
1373
+ clearTimeout(this.helloAckTimer);
1374
+ this.helloAckTimer = null;
1375
+ }
1376
+ for (const handler of this.connectHandlers) {
1377
+ try {
1378
+ handler();
1379
+ } catch {
1380
+ }
1381
+ }
1382
+ if (this.options.autoDrainOnConnect && this.options.client) {
1383
+ void this.drainOfflineEnvelopes();
1384
+ }
1385
+ }
1386
+ return;
1387
+ }
1388
+ if (this.isMessageNew(message)) {
1389
+ this.processOrderedMessage(message);
1390
+ return;
1391
+ }
1392
+ this.dispatch(message);
1393
+ };
1394
+ this.ws.onerror = () => {
1395
+ this.emitError(new ConnectionError("WebSocket error"));
1396
+ };
1397
+ this.ws.onclose = (event) => {
1398
+ if (this.helloAckTimer) {
1399
+ clearTimeout(this.helloAckTimer);
1400
+ this.helloAckTimer = null;
1401
+ }
1402
+ this.authenticated = false;
1403
+ for (const handler of this.disconnectHandlers) {
1404
+ try {
1405
+ handler({ code: event.code, reason: event.reason, wasClean: event.wasClean });
1406
+ } catch {
1407
+ }
1408
+ }
1409
+ this.resetOrderStates();
1410
+ this.scheduleReconnect();
1411
+ };
1412
+ }
1413
+ /**
1414
+ * Drain offline envelopes accumulated while the socket was disconnected.
1415
+ * Fires `message.new` for each, then acknowledges the highest
1416
+ * `delivery_id` so the server can prune its queue. Automatically
1417
+ * invoked on every successful `hello.ok` when `autoDrainOnConnect` is
1418
+ * enabled and a client is configured.
1419
+ *
1420
+ * Idempotent within a connection cycle — the server-side ack pointer
1421
+ * only moves forward, so concurrent or repeated calls are safe (only
1422
+ * the first pass yields envelopes; subsequent passes see an empty
1423
+ * queue).
1424
+ */
1425
+ async drainOfflineEnvelopes() {
1426
+ const client = this.options.client;
1427
+ if (!client) return;
1428
+ while (true) {
1429
+ let batch;
1430
+ try {
1431
+ batch = await client.sync();
1432
+ } catch (err) {
1433
+ this.emitError(err instanceof Error ? err : new ConnectionError("sync drain failed"));
1434
+ return;
1435
+ }
1436
+ if (batch.envelopes.length === 0) return;
1437
+ let highestDeliveryId = -1;
1438
+ for (const env of batch.envelopes) {
1439
+ if (env.delivery_id > highestDeliveryId) highestDeliveryId = env.delivery_id;
1440
+ const wrapped = {
1441
+ type: "message.new",
1442
+ payload: env.message
1443
+ };
1444
+ this.processOrderedMessage(wrapped);
1445
+ }
1446
+ if (highestDeliveryId >= 0) {
1447
+ try {
1448
+ await client.syncAck(highestDeliveryId);
1449
+ } catch (err) {
1450
+ this.emitError(err instanceof Error ? err : new ConnectionError("sync ack failed"));
1451
+ return;
1452
+ }
1453
+ }
1454
+ if (batch.envelopes.length < 100) return;
1455
+ }
1456
+ }
1457
+ scheduleReconnect() {
1458
+ if (this.disposed) return;
1459
+ if (!this.options.reconnect) return;
1460
+ if (this.reconnectAttempts >= this.options.maxReconnectAttempts) return;
1461
+ if (this.reconnectTimer) return;
1462
+ this.reconnectAttempts++;
1463
+ const delay = this.computeReconnectDelay(this.reconnectAttempts);
1464
+ this.reconnectTimer = setTimeout(() => {
1465
+ this.reconnectTimer = null;
1466
+ void this.connect().catch((err) => {
1467
+ this.emitError(err instanceof Error ? err : new ConnectionError(String(err)));
1468
+ });
1469
+ }, delay);
1470
+ }
1471
+ computeReconnectDelay(attempt) {
1472
+ const exp = this.options.reconnectInterval * Math.pow(2, Math.min(attempt - 1, 10));
1473
+ const capped = Math.min(exp, this.options.maxReconnectInterval);
1474
+ const jitter = 0.75 + Math.random() * 0.5;
1475
+ return Math.max(0, Math.floor(capped * jitter));
1476
+ }
1477
+ on(event, handler) {
1478
+ let handlers = this.handlers.get(event);
1479
+ if (!handlers) {
1480
+ handlers = /* @__PURE__ */ new Set();
1481
+ this.handlers.set(event, handlers);
1482
+ }
1483
+ handlers.add(handler);
1484
+ return () => {
1485
+ handlers.delete(handler);
1486
+ if (handlers.size === 0) this.handlers.delete(event);
1487
+ };
1488
+ }
1489
+ onError(handler) {
1490
+ this.errorHandlers.add(handler);
1491
+ return () => this.errorHandlers.delete(handler);
1492
+ }
1493
+ /** Fires each time the handshake completes (initial + every reconnect). */
1494
+ onConnect(handler) {
1495
+ this.connectHandlers.add(handler);
1496
+ return () => this.connectHandlers.delete(handler);
1497
+ }
1498
+ /** Fires on every socket close, regardless of reason (clean or error). */
1499
+ onDisconnect(handler) {
1500
+ this.disconnectHandlers.add(handler);
1501
+ return () => this.disconnectHandlers.delete(handler);
1502
+ }
1503
+ send(message) {
1504
+ if (!this.ws || this.ws.readyState !== 1 || !this.authenticated) {
1505
+ throw new ConnectionError("WebSocket is not connected");
1506
+ }
1507
+ this.ws.send(JSON.stringify(message));
1508
+ }
1509
+ /**
1510
+ * Announce that the caller has started composing in `conversationId`.
1511
+ * Fire-and-forget: server broadcasts a `typing.start` event to every
1512
+ * other participant but does not ACK. Pair with `sendTypingStop` when
1513
+ * the agent finishes composing or navigates away. Throws
1514
+ * `ConnectionError` if the socket is not open.
1515
+ */
1516
+ sendTypingStart(conversationId) {
1517
+ this.send({ type: "typing.start", payload: { conversation_id: conversationId } });
1518
+ }
1519
+ /** Counterpart to `sendTypingStart`. */
1520
+ sendTypingStop(conversationId) {
1521
+ this.send({ type: "typing.stop", payload: { conversation_id: conversationId } });
1522
+ }
1523
+ /**
1524
+ * Push a read receipt. `throughSeq` means "every message up to and
1525
+ * including this seq is read". The server fans out a `message.read`
1526
+ * event to other participants. Cheap to call repeatedly; send the
1527
+ * highest seq observed per conversation.
1528
+ */
1529
+ sendReadAck(conversationId, throughSeq) {
1530
+ this.send({
1531
+ type: "message.read_ack",
1532
+ payload: { conversation_id: conversationId, through_seq: throughSeq }
1533
+ });
1534
+ }
1535
+ /** `true` after a completed HELLO handshake and before the next close. */
1536
+ get isConnected() {
1537
+ return this.authenticated && this.ws?.readyState === 1;
1538
+ }
1539
+ /**
1540
+ * Close the socket, disable auto-reconnect, and release all handlers.
1541
+ * After calling this, `connect()` throws — create a fresh
1542
+ * `RealtimeClient` if you want to reopen.
1543
+ */
1544
+ disconnect() {
1545
+ this.disposed = true;
1546
+ this.options.reconnect = false;
1547
+ if (this.reconnectTimer) {
1548
+ clearTimeout(this.reconnectTimer);
1549
+ this.reconnectTimer = null;
1550
+ }
1551
+ if (this.helloAckTimer) {
1552
+ clearTimeout(this.helloAckTimer);
1553
+ this.helloAckTimer = null;
1554
+ }
1555
+ this.drainAllPendingForShutdown();
1556
+ try {
1557
+ this.ws?.close();
1558
+ } catch {
1559
+ }
1560
+ this.ws = null;
1561
+ this.authenticated = false;
1562
+ this.handlers.clear();
1563
+ this.errorHandlers.clear();
1564
+ this.connectHandlers.clear();
1565
+ this.disconnectHandlers.clear();
1566
+ }
1567
+ emitError(error) {
1568
+ for (const handler of this.errorHandlers) {
1569
+ handler(error);
1570
+ }
1571
+ }
1572
+ dispatch(message) {
1573
+ const handlers = this.handlers.get(message.type);
1574
+ if (!handlers) return;
1575
+ for (const handler of handlers) {
1576
+ handler(message);
1577
+ }
1578
+ }
1579
+ isMessageNew(message) {
1580
+ return message.type === "message.new";
1581
+ }
1582
+ // ─── Per-conversation seq ordering ───────────────────────────────────────
1583
+ //
1584
+ // Invariant: for any conversation_id, handlers see message.new envelopes
1585
+ // strictly in seq-ascending order with no skipped or repeated seqs (modulo
1586
+ // the gap-fill failure path, where we surface the incident via
1587
+ // onSequenceGap and continue forward).
1588
+ //
1589
+ // Why per-conversation instead of global: seq numbers are minted per
1590
+ // conversation by send_message_atomic, so cross-conversation arrivals
1591
+ // have no ordering relationship to enforce.
1592
+ processOrderedMessage(message) {
1593
+ const payload = message.payload;
1594
+ const conversationId = payload?.conversation_id;
1595
+ if (typeof conversationId !== "string") {
1596
+ this.dispatch(message);
1597
+ return;
1598
+ }
1599
+ const seq = this.extractSeq(message);
1600
+ if (seq === null) {
1601
+ this.dispatch(message);
1602
+ return;
1603
+ }
1604
+ const state = this.getOrCreateOrderState(conversationId);
1605
+ if (state.nextExpectedSeq === null) {
1606
+ state.nextExpectedSeq = seq + 1;
1607
+ this.dispatch(message);
1608
+ return;
1609
+ }
1610
+ if (seq < state.nextExpectedSeq) {
1611
+ return;
1612
+ }
1613
+ if (seq === state.nextExpectedSeq) {
1614
+ this.dispatch(message);
1615
+ state.nextExpectedSeq = seq + 1;
1616
+ this.drainConsecutive(conversationId, state);
1617
+ this.maybeClearGapTimer(state);
1618
+ this.cleanupIfIdle(conversationId, state);
1619
+ return;
1620
+ }
1621
+ state.buffer.set(seq, message);
1622
+ if (state.buffer.size > MAX_BUFFERED_PER_CONVERSATION) {
1623
+ this.resolveGap(conversationId, state, {
1624
+ recovered: false,
1625
+ reason: "buffer_overflow",
1626
+ bufferedSeq: this.minBufferedSeq(state)
1627
+ });
1628
+ return;
1629
+ }
1630
+ if (state.gapTimer === null) {
1631
+ state.gapStartedAt = Date.now();
1632
+ state.gapStartedExpectedSeq = state.nextExpectedSeq;
1633
+ state.gapTimer = setTimeout(() => {
1634
+ void this.handleGapTimer(conversationId);
1635
+ }, GAP_FILL_WINDOW_MS);
1636
+ }
1637
+ }
1638
+ async handleGapTimer(conversationId) {
1639
+ const state = this.orderStates.get(conversationId);
1640
+ if (!state) return;
1641
+ state.gapTimer = null;
1642
+ if (state.buffer.size === 0) {
1643
+ this.cleanupIfIdle(conversationId, state);
1644
+ return;
1645
+ }
1646
+ const expectedSeq = state.nextExpectedSeq;
1647
+ if (expectedSeq === null) return;
1648
+ if (!this.options.client) {
1649
+ this.resolveGap(conversationId, state, {
1650
+ recovered: false,
1651
+ reason: "gap_fill_unavailable",
1652
+ bufferedSeq: this.minBufferedSeq(state)
1653
+ });
1654
+ return;
1655
+ }
1656
+ if (state.gapFillInFlight) return;
1657
+ state.gapFillInFlight = true;
1658
+ let fetched = [];
1659
+ let fillError = false;
1660
+ try {
1661
+ fetched = await this.options.client.getMessages(conversationId, {
1662
+ afterSeq: expectedSeq - 1,
1663
+ limit: GAP_FILL_LIMIT
1664
+ });
1665
+ } catch {
1666
+ fillError = true;
1667
+ } finally {
1668
+ state.gapFillInFlight = false;
1669
+ }
1670
+ const stateNow = this.orderStates.get(conversationId);
1671
+ if (!stateNow || stateNow !== state) return;
1672
+ if (fillError) {
1673
+ this.resolveGap(conversationId, state, {
1674
+ recovered: false,
1675
+ reason: "gap_fill_failed",
1676
+ bufferedSeq: this.minBufferedSeq(state)
1677
+ });
1678
+ return;
1679
+ }
1680
+ for (const row of fetched) {
1681
+ const rowSeq = typeof row.seq === "number" ? row.seq : null;
1682
+ if (rowSeq === null || rowSeq < expectedSeq) continue;
1683
+ if (state.buffer.has(rowSeq)) continue;
1684
+ state.buffer.set(rowSeq, {
1685
+ type: "message.new",
1686
+ payload: row
1687
+ });
1688
+ }
1689
+ const drainedThroughGap = this.drainConsecutive(conversationId, state);
1690
+ if (drainedThroughGap) {
1691
+ this.resolveGap(conversationId, state, {
1692
+ recovered: true,
1693
+ reason: "gap_filled",
1694
+ bufferedSeq: null
1695
+ });
1696
+ } else {
1697
+ this.resolveGap(conversationId, state, {
1698
+ recovered: false,
1699
+ reason: "gap_fill_failed",
1700
+ bufferedSeq: this.minBufferedSeq(state)
1701
+ });
1702
+ }
1703
+ }
1704
+ // Returns true if we drained at least one message past the original
1705
+ // expected seq (i.e. the gap is closed for now). Returns false if the
1706
+ // expected seq still isn't in the buffer — caller decides what to do.
1707
+ drainConsecutive(conversationId, state) {
1708
+ if (state.nextExpectedSeq === null) return false;
1709
+ let drained = false;
1710
+ while (state.buffer.has(state.nextExpectedSeq)) {
1711
+ const msg = state.buffer.get(state.nextExpectedSeq);
1712
+ state.buffer.delete(state.nextExpectedSeq);
1713
+ this.dispatch(msg);
1714
+ state.nextExpectedSeq += 1;
1715
+ drained = true;
1716
+ }
1717
+ if (drained) this.cleanupIfIdle(conversationId, state);
1718
+ return drained;
1719
+ }
1720
+ // Force-resolve a gap by dispatching every buffered message in seq
1721
+ // order, advancing nextExpectedSeq past the highest, and firing the
1722
+ // onSequenceGap callback. Used for unrecoverable cases (no client,
1723
+ // fetch failed, buffer overflow).
1724
+ resolveGap(conversationId, state, info) {
1725
+ const expectedSeq = state.gapStartedExpectedSeq ?? state.nextExpectedSeq ?? 0;
1726
+ const gapMs = state.gapStartedAt !== null ? Date.now() - state.gapStartedAt : 0;
1727
+ const seqs = Array.from(state.buffer.keys()).sort((a, b) => a - b);
1728
+ let highestDispatched = state.nextExpectedSeq !== null ? state.nextExpectedSeq - 1 : -1;
1729
+ for (const s of seqs) {
1730
+ const msg = state.buffer.get(s);
1731
+ this.dispatch(msg);
1732
+ if (s > highestDispatched) highestDispatched = s;
1733
+ }
1734
+ state.buffer.clear();
1735
+ if (highestDispatched >= 0) {
1736
+ state.nextExpectedSeq = highestDispatched + 1;
1737
+ }
1738
+ if (state.gapTimer !== null) {
1739
+ clearTimeout(state.gapTimer);
1740
+ state.gapTimer = null;
1741
+ }
1742
+ state.gapStartedAt = null;
1743
+ state.gapStartedExpectedSeq = null;
1744
+ this.options.onSequenceGap?.({
1745
+ conversationId,
1746
+ expectedSeq,
1747
+ bufferedSeq: info.bufferedSeq,
1748
+ gapMs,
1749
+ recovered: info.recovered,
1750
+ reason: info.reason
1751
+ });
1752
+ this.cleanupIfIdle(conversationId, state);
1753
+ }
1754
+ maybeClearGapTimer(state) {
1755
+ if (state.gapTimer !== null && state.buffer.size === 0) {
1756
+ clearTimeout(state.gapTimer);
1757
+ state.gapTimer = null;
1758
+ state.gapStartedAt = null;
1759
+ state.gapStartedExpectedSeq = null;
1760
+ }
1761
+ }
1762
+ getOrCreateOrderState(conversationId) {
1763
+ let state = this.orderStates.get(conversationId);
1764
+ if (!state) {
1765
+ state = {
1766
+ nextExpectedSeq: null,
1767
+ buffer: /* @__PURE__ */ new Map(),
1768
+ gapTimer: null,
1769
+ gapStartedAt: null,
1770
+ gapStartedExpectedSeq: null,
1771
+ gapFillInFlight: false
1772
+ };
1773
+ this.orderStates.set(conversationId, state);
1774
+ }
1775
+ return state;
1776
+ }
1777
+ // Drop the per-conversation entry once it's quiescent (no buffered
1778
+ // messages, no pending gap timer, no in-flight fetch). Keeps the map
1779
+ // bounded — without this, every conversation an agent ever touches
1780
+ // would leave a stale entry alive for the lifetime of the connection.
1781
+ cleanupIfIdle(conversationId, state) {
1782
+ if (state.buffer.size === 0 && state.gapTimer === null && !state.gapFillInFlight) {
1783
+ this.orderStates.delete(conversationId);
1784
+ }
1785
+ }
1786
+ extractSeq(message) {
1787
+ const seq = message.payload?.seq;
1788
+ return typeof seq === "number" && Number.isFinite(seq) ? seq : null;
1789
+ }
1790
+ minBufferedSeq(state) {
1791
+ if (state.buffer.size === 0) return null;
1792
+ let min = Infinity;
1793
+ for (const k of state.buffer.keys()) if (k < min) min = k;
1794
+ return Number.isFinite(min) ? min : null;
1795
+ }
1796
+ resetOrderStates() {
1797
+ for (const state of this.orderStates.values()) {
1798
+ if (state.gapTimer !== null) clearTimeout(state.gapTimer);
1799
+ }
1800
+ this.orderStates.clear();
1801
+ }
1802
+ drainAllPendingForShutdown() {
1803
+ for (const [conversationId, state] of this.orderStates) {
1804
+ if (state.gapTimer !== null) {
1805
+ clearTimeout(state.gapTimer);
1806
+ state.gapTimer = null;
1807
+ }
1808
+ if (state.buffer.size === 0) continue;
1809
+ const seqs = Array.from(state.buffer.keys()).sort((a, b) => a - b);
1810
+ for (const s of seqs) this.dispatch(state.buffer.get(s));
1811
+ state.buffer.clear();
1812
+ this.options.onSequenceGap?.({
1813
+ conversationId,
1814
+ expectedSeq: state.gapStartedExpectedSeq ?? state.nextExpectedSeq ?? 0,
1815
+ bufferedSeq: seqs[0] ?? null,
1816
+ gapMs: state.gapStartedAt !== null ? Date.now() - state.gapStartedAt : 0,
1817
+ recovered: false,
1818
+ reason: "gap_fill_unavailable"
1819
+ });
1820
+ }
1821
+ this.orderStates.clear();
1822
+ }
1823
+ };
1824
+
1825
+ // src/webhook-verify.ts
1826
+ var WebhookVerificationError = class extends Error {
1827
+ reason;
1828
+ constructor(reason, message) {
1829
+ super(message ?? reason);
1830
+ this.name = "WebhookVerificationError";
1831
+ this.reason = reason;
1832
+ }
1833
+ };
1834
+ async function verifyWebhook(options) {
1835
+ const { payload, signature, secret, toleranceSeconds = 300 } = options;
1836
+ const now2 = options.now ?? Date.now;
1837
+ if (!signature) {
1838
+ throw new WebhookVerificationError("missing_signature");
1839
+ }
1840
+ const parsed = parseSignatureHeader(signature);
1841
+ const bodyString = typeof payload === "string" ? payload : new TextDecoder().decode(payload);
1842
+ let expectedMessage;
1843
+ if (parsed.timestamp !== null) {
1844
+ if (toleranceSeconds > 0) {
1845
+ const ageSeconds = Math.abs(now2() / 1e3 - parsed.timestamp);
1846
+ if (ageSeconds > toleranceSeconds) {
1847
+ throw new WebhookVerificationError("timestamp_skew");
1848
+ }
1849
+ }
1850
+ expectedMessage = `${parsed.timestamp}.${bodyString}`;
1851
+ } else {
1852
+ expectedMessage = bodyString;
1853
+ }
1854
+ const computed = await hmacSha256Hex(secret, expectedMessage);
1855
+ if (!constantTimeEqual(computed, parsed.digest)) {
1856
+ throw new WebhookVerificationError("bad_signature");
1857
+ }
1858
+ try {
1859
+ const json = JSON.parse(bodyString);
1860
+ return json;
1861
+ } catch {
1862
+ throw new WebhookVerificationError("malformed_payload");
1863
+ }
1864
+ }
1865
+ function parseSignatureHeader(header) {
1866
+ const trimmed = header.trim();
1867
+ if (trimmed.includes("=")) {
1868
+ const parts = trimmed.split(",");
1869
+ let timestamp = null;
1870
+ let digest2 = null;
1871
+ for (const p of parts) {
1872
+ const idx = p.indexOf("=");
1873
+ if (idx <= 0) continue;
1874
+ const key = p.slice(0, idx).trim();
1875
+ const value = p.slice(idx + 1).trim();
1876
+ if (key === "t") {
1877
+ const n = Number(value);
1878
+ if (Number.isFinite(n)) timestamp = n;
1879
+ } else if (key === "v1") {
1880
+ digest2 = value.toLowerCase();
1881
+ }
1882
+ }
1883
+ if (!digest2 || !/^[a-f0-9]+$/.test(digest2)) {
1884
+ throw new WebhookVerificationError("malformed_signature");
1885
+ }
1886
+ return { timestamp, digest: digest2 };
1887
+ }
1888
+ const digest = trimmed.toLowerCase();
1889
+ if (!/^[a-f0-9]+$/.test(digest)) {
1890
+ throw new WebhookVerificationError("malformed_signature");
1891
+ }
1892
+ return { timestamp: null, digest };
1893
+ }
1894
+ async function hmacSha256Hex(secret, message) {
1895
+ const subtle = globalThis.crypto?.subtle;
1896
+ if (!subtle) {
1897
+ throw new WebhookVerificationError(
1898
+ "bad_signature",
1899
+ "Web Crypto API not available in this runtime; webhook verification requires `globalThis.crypto.subtle`."
1900
+ );
1901
+ }
1902
+ const enc = new TextEncoder();
1903
+ const key = await subtle.importKey(
1904
+ "raw",
1905
+ enc.encode(secret),
1906
+ { name: "HMAC", hash: "SHA-256" },
1907
+ false,
1908
+ ["sign"]
1909
+ );
1910
+ const sig = await subtle.sign("HMAC", key, enc.encode(message));
1911
+ const bytes = new Uint8Array(sig);
1912
+ let hex = "";
1913
+ for (const b of bytes) hex += b.toString(16).padStart(2, "0");
1914
+ return hex;
1915
+ }
1916
+ function constantTimeEqual(a, b) {
1917
+ if (a.length !== b.length) return false;
1918
+ let mismatch = 0;
1919
+ for (let i = 0; i < a.length; i++) {
1920
+ mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
1921
+ }
1922
+ return mismatch === 0;
1923
+ }
1924
+
1925
+ // src/types/attachment.ts
1926
+ var MAX_ATTACHMENT_SIZE = 25 * 1024 * 1024;
1927
+ var ALLOWED_ATTACHMENT_MIME = [
1928
+ "image/png",
1929
+ "image/jpeg",
1930
+ "image/gif",
1931
+ "image/webp",
1932
+ "application/pdf",
1933
+ "application/json",
1934
+ "text/plain",
1935
+ "text/markdown",
1936
+ "text/csv",
1937
+ "audio/mpeg",
1938
+ "audio/wav",
1939
+ "audio/ogg",
1940
+ "video/mp4",
1941
+ "video/webm"
1942
+ ];
1943
+
1944
+ export { ALLOWED_ATTACHMENT_MIME, AgentChatClient, AgentChatError, AwaitingReplyError, BlockedError, ConnectionError, DEFAULT_RETRY_POLICY, ErrorCode, ForbiddenError, GroupDeletedError, HttpTransport, MAX_ATTACHMENT_SIZE, NotFoundError, RateLimitedError, RealtimeClient, RecipientBackloggedError, RestrictedError, ServerError, SuspendedError, UnauthorizedError, VERSION, ValidationError, WebhookVerificationError, createAgentChatError, paginate, parseRetryAfter, verifyWebhook };
1945
+ //# sourceMappingURL=index.js.map
1946
+ //# sourceMappingURL=index.js.map