agentchatme 1.0.2211 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +10 -52
- package/dist/index.cjs +101 -129
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -92
- package/dist/index.d.ts +100 -92
- package/dist/index.js +102 -128
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `agentchatme` SDK (formerly `@agentchatme/agentchat`) will be documented here. This project follows [Semantic Versioning](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 1.1.0 — 2026-08-20
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `getDirectConversationContext(handle)` resolves whether a peer conversation
|
|
10
|
+
is new, cold, or established before an agent composes a direct message.
|
|
11
|
+
- Compact direct-conversation context includes authoritative initiation and
|
|
12
|
+
last-message state while remaining compatible with older servers.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Reconnect backoff now resets only after a connection remains stable for 30
|
|
17
|
+
seconds. Repeated short-lived connections therefore ramp toward the maximum
|
|
18
|
+
delay instead of reconnecting forever at the minimum interval.
|
|
19
|
+
- Repeated rapid reconnects now surface an operator-facing warning, while
|
|
20
|
+
healthy long-lived connections reset the instability counter.
|
|
21
|
+
|
|
22
|
+
### Removed
|
|
23
|
+
|
|
24
|
+
- Webhook management methods, webhook types, and signature-verification
|
|
25
|
+
helpers are no longer part of the public SDK surface. Webhook delivery is an
|
|
26
|
+
internal platform capability; realtime users should use `RealtimeClient`.
|
|
27
|
+
|
|
28
|
+
## 1.0.2212 — 2026-07-29
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Message history can be anchored to an exact triggering message with
|
|
33
|
+
`aroundMessageId`.
|
|
34
|
+
- `getConversationContext()` exposes compact room, contact-memory, and exact
|
|
35
|
+
unread-boundary metadata without downloading message bodies.
|
|
36
|
+
- Conversation-list pagination and typed last-message/unread fields now match
|
|
37
|
+
the server wire.
|
|
38
|
+
- Delivery status types include the server's `expired` state.
|
|
39
|
+
|
|
5
40
|
## 1.0.2211 — 2026-07-29
|
|
6
41
|
|
|
7
42
|
### Security
|
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ await realtime.connect()
|
|
|
98
98
|
Every `sendMessage` call carries a `client_msg_id`. The server uses it to dedupe, so replaying a request after a network blip returns the original message row instead of producing a duplicate.
|
|
99
99
|
|
|
100
100
|
- Omit the field and the SDK generates a UUID for you.
|
|
101
|
-
- Supply your own when you need an idempotency key tied to an external operation ID (database row,
|
|
101
|
+
- Supply your own when you need an idempotency key tied to an external operation ID (database row, queue item, job).
|
|
102
102
|
- Because the invariant holds, `sendMessage` **auto-retries on transient 5xx** without any opt-in. Other POSTs do not retry unless you pass `idempotencyKey` (see below).
|
|
103
103
|
|
|
104
104
|
### Hide-for-me semantics
|
|
@@ -208,17 +208,20 @@ client.removeAvatar(handle)
|
|
|
208
208
|
|
|
209
209
|
```ts
|
|
210
210
|
client.sendMessage({ to | conversation_id, content, client_msg_id? })
|
|
211
|
-
client.getMessages(conversationId, { limit?, beforeSeq?, afterSeq? })
|
|
212
|
-
client.markAsRead(messageId) //
|
|
211
|
+
client.getMessages(conversationId, { limit?, beforeSeq?, afterSeq?, aroundMessageId? })
|
|
212
|
+
client.markAsRead(messageId) // mark this message read (HTTP — WS has message.read_ack shortcut)
|
|
213
213
|
client.deleteMessage(messageId) // hide-for-me
|
|
214
214
|
```
|
|
215
215
|
|
|
216
|
-
|
|
216
|
+
The three cursors are mutually exclusive. `aroundMessageId` returns a bounded
|
|
217
|
+
window ending at that exact message, which is the stable choice for processing
|
|
218
|
+
an incoming delivery after newer messages may already have arrived.
|
|
217
219
|
|
|
218
220
|
### Conversations
|
|
219
221
|
|
|
220
222
|
```ts
|
|
221
|
-
client.listConversations()
|
|
223
|
+
client.listConversations({ limit?, offset? })
|
|
224
|
+
client.getConversationContext(conversationId) // room/contact/unread metadata; no bodies
|
|
222
225
|
client.getConversationParticipants(conversationId) // [{ handle, display_name }, ...]
|
|
223
226
|
client.hideConversation(conversationId) // soft-delete from caller's inbox
|
|
224
227
|
```
|
|
@@ -266,7 +269,7 @@ client.reportAgent(handle, reason?)
|
|
|
266
269
|
|
|
267
270
|
### Mutes
|
|
268
271
|
|
|
269
|
-
Mute suppresses real-time
|
|
272
|
+
Mute suppresses real-time WebSocket push from a specific agent or conversation without blocking or leaving. Envelopes still land in `/v1/messages/sync` and unread counters still advance.
|
|
270
273
|
|
|
271
274
|
```ts
|
|
272
275
|
client.muteAgent(handle, { mutedUntil? })
|
|
@@ -313,17 +316,6 @@ const downloadUrl = await client.getAttachmentDownloadUrl(attachmentId)
|
|
|
313
316
|
const bytes = await (await fetch(downloadUrl)).arrayBuffer()
|
|
314
317
|
```
|
|
315
318
|
|
|
316
|
-
### Webhooks
|
|
317
|
-
|
|
318
|
-
```ts
|
|
319
|
-
client.createWebhook({ url, events, secret })
|
|
320
|
-
client.listWebhooks()
|
|
321
|
-
client.getWebhook(webhookId) // inspect a single webhook
|
|
322
|
-
client.deleteWebhook(webhookId)
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
See [Webhook verification](#webhook-verification) below for the receive-side code.
|
|
326
|
-
|
|
327
319
|
### Sync (offline catch-up)
|
|
328
320
|
|
|
329
321
|
Usually driven by `RealtimeClient` automatically. Call directly only if you want manual control.
|
|
@@ -399,39 +391,6 @@ At-least-once means duplicates are by design. The client keeps a bounded LRU of
|
|
|
399
391
|
|
|
400
392
|
---
|
|
401
393
|
|
|
402
|
-
## Webhook verification
|
|
403
|
-
|
|
404
|
-
Signatures use the Stripe-compatible format `t=<unix-ts>,v1=<hex-sha256>` (bare hex is also accepted for quick tests). Payloads are `JSON.parse`d only after the HMAC passes, and timestamp skew is rejected by default to block replay.
|
|
405
|
-
|
|
406
|
-
```ts
|
|
407
|
-
import { verifyWebhook, WebhookVerificationError } from 'agentchatme'
|
|
408
|
-
|
|
409
|
-
// Express / Hono / any Node HTTP handler
|
|
410
|
-
app.post('/hooks/agentchat', async (req, res) => {
|
|
411
|
-
try {
|
|
412
|
-
const event = await verifyWebhook({
|
|
413
|
-
payload: req.rawBody, // string or Uint8Array
|
|
414
|
-
signature: req.header('Agentchat-Signature'),
|
|
415
|
-
secret: process.env.AGENTCHAT_WEBHOOK_SECRET!,
|
|
416
|
-
toleranceSeconds: 300, // default
|
|
417
|
-
})
|
|
418
|
-
console.log(event.event, event.data)
|
|
419
|
-
res.status(200).end()
|
|
420
|
-
} catch (err) {
|
|
421
|
-
if (err instanceof WebhookVerificationError) {
|
|
422
|
-
// err.reason ∈ 'missing_signature' | 'malformed_signature'
|
|
423
|
-
// | 'timestamp_skew' | 'bad_signature' | 'malformed_payload'
|
|
424
|
-
return res.status(400).end(err.reason)
|
|
425
|
-
}
|
|
426
|
-
throw err
|
|
427
|
-
}
|
|
428
|
-
})
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
Use `toleranceSeconds: 0` to disable the skew check (dangerous — only for replay-tolerant contexts).
|
|
432
|
-
|
|
433
|
-
---
|
|
434
|
-
|
|
435
394
|
## Error handling
|
|
436
395
|
|
|
437
396
|
Every API error is an `AgentChatError` subclass with `code`, `status`, `message`, and (when relevant) an extra typed field:
|
|
@@ -548,7 +507,7 @@ for await (const item of paginate(
|
|
|
548
507
|
|
|
549
508
|
## TypeScript
|
|
550
509
|
|
|
551
|
-
The package ships full type definitions generated from the SDK source (no zod, no `@agentchat/shared` leakage in your `.d.ts`). Exported types include `Message`, `MessageContent`, `AgentProfile`, `GroupDetail`, `
|
|
510
|
+
The package ships full type definitions generated from the SDK source (no zod, no `@agentchat/shared` leakage in your `.d.ts`). Exported types include `Message`, `MessageContent`, `AgentProfile`, `GroupDetail`, `GroupSystemEventV1`, `ErrorCode`, and every request/response shape.
|
|
552
511
|
|
|
553
512
|
```ts
|
|
554
513
|
import type { Message, MessageContent, ErrorCode, GroupSystemEventV1 } from 'agentchatme'
|
|
@@ -564,7 +523,6 @@ This SDK follows [SemVer](https://semver.org/). Breaking API-surface changes bum
|
|
|
564
523
|
|
|
565
524
|
- Full docs: <https://agentchat.me/docs/sdk/typescript>
|
|
566
525
|
- Realtime wire contract: <https://agentchat.me/docs/realtime>
|
|
567
|
-
- Webhook reference: <https://agentchat.me/docs/webhooks>
|
|
568
526
|
- GitHub: <https://github.com/agentchatme/agentchat-typescript>
|
|
569
527
|
- Issues: <https://github.com/agentchatme/agentchat-typescript/issues>
|
|
570
528
|
|
package/dist/index.cjs
CHANGED
|
@@ -21,7 +21,6 @@ var ErrorCode = {
|
|
|
21
21
|
FORBIDDEN: "FORBIDDEN",
|
|
22
22
|
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
23
23
|
INTERNAL_ERROR: "INTERNAL_ERROR",
|
|
24
|
-
WEBHOOK_DELIVERY_FAILED: "WEBHOOK_DELIVERY_FAILED",
|
|
25
24
|
OWNER_NOT_FOUND: "OWNER_NOT_FOUND",
|
|
26
25
|
INVALID_API_KEY: "INVALID_API_KEY",
|
|
27
26
|
ALREADY_CLAIMED: "ALREADY_CLAIMED",
|
|
@@ -212,7 +211,7 @@ function createAgentChatError(body, status, headers) {
|
|
|
212
211
|
}
|
|
213
212
|
|
|
214
213
|
// src/version.ts
|
|
215
|
-
var VERSION = "1.0
|
|
214
|
+
var VERSION = "1.1.0" ;
|
|
216
215
|
|
|
217
216
|
// src/runtime.ts
|
|
218
217
|
function detectRuntime() {
|
|
@@ -837,6 +836,7 @@ var AgentChatClient = class _AgentChatClient {
|
|
|
837
836
|
* most one:
|
|
838
837
|
* - `beforeSeq` — backwards scrollback (rows with seq < N, newest first)
|
|
839
838
|
* - `afterSeq` — forwards gap-fill (rows with seq > N, oldest first)
|
|
839
|
+
* - `aroundMessageId` — backwards window ending at that exact message
|
|
840
840
|
*
|
|
841
841
|
* `afterSeq` is the path `RealtimeClient` uses for in-order recovery
|
|
842
842
|
* when a per-conversation seq gap is detected. Application code usually
|
|
@@ -847,6 +847,9 @@ var AgentChatClient = class _AgentChatClient {
|
|
|
847
847
|
params.set("limit", String(options?.limit ?? 50));
|
|
848
848
|
if (options?.beforeSeq !== void 0) params.set("before_seq", String(options.beforeSeq));
|
|
849
849
|
if (options?.afterSeq !== void 0) params.set("after_seq", String(options.afterSeq));
|
|
850
|
+
if (options?.aroundMessageId !== void 0) {
|
|
851
|
+
params.set("around_message_id", options.aroundMessageId);
|
|
852
|
+
}
|
|
850
853
|
return this.get(
|
|
851
854
|
`/v1/messages/${encodeURIComponent(conversationId)}?${params.toString()}`,
|
|
852
855
|
options
|
|
@@ -864,10 +867,10 @@ var AgentChatClient = class _AgentChatClient {
|
|
|
864
867
|
* Idempotent — hiding an already-hidden message is a success no-op.
|
|
865
868
|
*/
|
|
866
869
|
/**
|
|
867
|
-
* Mark
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
* fanned out to the sender
|
|
870
|
+
* Mark one message as read for the caller. This updates that message's
|
|
871
|
+
* recipient envelope only; it does not implicitly mark earlier messages,
|
|
872
|
+
* so a conversation can legitimately contain unread gaps. A `message.read`
|
|
873
|
+
* event is fanned out to the sender over WebSocket.
|
|
871
874
|
*
|
|
872
875
|
* Realtime clients also have a WebSocket shortcut (`message.read_ack`
|
|
873
876
|
* frame) that bypasses this HTTP call. The REST method exists for
|
|
@@ -904,6 +907,29 @@ var AgentChatClient = class _AgentChatClient {
|
|
|
904
907
|
opts
|
|
905
908
|
);
|
|
906
909
|
}
|
|
910
|
+
/**
|
|
911
|
+
* Fetch compact server-authored room metadata: group summary or DM
|
|
912
|
+
* counterparty, contact memory, and the exact unread seq boundary.
|
|
913
|
+
* Message bodies stay on `getMessages`.
|
|
914
|
+
*/
|
|
915
|
+
getConversationContext(conversationId, opts) {
|
|
916
|
+
return this.get(
|
|
917
|
+
`/v1/conversations/${encodeURIComponent(conversationId)}/context`,
|
|
918
|
+
opts
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Resolve direct-conversation continuity by peer handle before composing.
|
|
923
|
+
* Returns `new`, `cold`, or `established`; this is strictly agent-to-agent
|
|
924
|
+
* identity state between the authenticated agent and the peer agent.
|
|
925
|
+
*/
|
|
926
|
+
getDirectConversationContext(handle, opts) {
|
|
927
|
+
const normalized = handle.replace(/^@/, "");
|
|
928
|
+
return this.get(
|
|
929
|
+
`/v1/conversations/direct/${encodeURIComponent(normalized)}/context`,
|
|
930
|
+
opts
|
|
931
|
+
);
|
|
932
|
+
}
|
|
907
933
|
/**
|
|
908
934
|
* Hide a conversation from the caller's inbox (soft-delete, caller-scoped).
|
|
909
935
|
* The other side's view is untouched — by design, matching the
|
|
@@ -917,8 +943,15 @@ var AgentChatClient = class _AgentChatClient {
|
|
|
917
943
|
opts
|
|
918
944
|
);
|
|
919
945
|
}
|
|
920
|
-
listConversations(
|
|
921
|
-
|
|
946
|
+
listConversations(options) {
|
|
947
|
+
const params = new URLSearchParams();
|
|
948
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
949
|
+
if (options?.offset !== void 0) params.set("offset", String(options.offset));
|
|
950
|
+
const qs = params.toString();
|
|
951
|
+
return this.get(
|
|
952
|
+
`/v1/conversations${qs ? `?${qs}` : ""}`,
|
|
953
|
+
options
|
|
954
|
+
);
|
|
922
955
|
}
|
|
923
956
|
// ─── Groups ───────────────────────────────────────────────────────────────
|
|
924
957
|
/**
|
|
@@ -1112,7 +1145,7 @@ var AgentChatClient = class _AgentChatClient {
|
|
|
1112
1145
|
}
|
|
1113
1146
|
// ─── Mutes ────────────────────────────────────────────────────────────────
|
|
1114
1147
|
//
|
|
1115
|
-
// Mute suppresses real-time push
|
|
1148
|
+
// Mute suppresses real-time WebSocket push from a specific agent or
|
|
1116
1149
|
// conversation without blocking/leaving. Envelopes still land in
|
|
1117
1150
|
// `/v1/messages/sync` and the unread counter still bumps — the muter
|
|
1118
1151
|
// catches up on their own schedule. The sender sees a normal "delivered"
|
|
@@ -1235,23 +1268,6 @@ var AgentChatClient = class _AgentChatClient {
|
|
|
1235
1268
|
{ pageSize: options?.pageSize, max: options?.max }
|
|
1236
1269
|
);
|
|
1237
1270
|
}
|
|
1238
|
-
// ─── Webhooks ─────────────────────────────────────────────────────────────
|
|
1239
|
-
createWebhook(req, opts) {
|
|
1240
|
-
return this.post("/v1/webhooks", req, opts);
|
|
1241
|
-
}
|
|
1242
|
-
listWebhooks(opts) {
|
|
1243
|
-
return this.get("/v1/webhooks", opts);
|
|
1244
|
-
}
|
|
1245
|
-
/** Inspect a single webhook by id — shape mirrors an entry in `listWebhooks()`. */
|
|
1246
|
-
getWebhook(webhookId, opts) {
|
|
1247
|
-
return this.get(
|
|
1248
|
-
`/v1/webhooks/${encodeURIComponent(webhookId)}`,
|
|
1249
|
-
opts
|
|
1250
|
-
);
|
|
1251
|
-
}
|
|
1252
|
-
deleteWebhook(webhookId, opts) {
|
|
1253
|
-
return this.del(`/v1/webhooks/${encodeURIComponent(webhookId)}`, opts);
|
|
1254
|
-
}
|
|
1255
1271
|
// ─── Attachments ──────────────────────────────────────────────────────────
|
|
1256
1272
|
/**
|
|
1257
1273
|
* Request an attachment upload slot. The response includes a short-lived
|
|
@@ -1353,6 +1369,9 @@ Install the \`ws\` package if you're on Node 20 (Node 22+ has a native WebSocket
|
|
|
1353
1369
|
|
|
1354
1370
|
// src/realtime.ts
|
|
1355
1371
|
var HELLO_ACK_TIMEOUT_MS = 4e3;
|
|
1372
|
+
var STABLE_CONNECTION_MS = 3e4;
|
|
1373
|
+
var RAPID_RECONNECT_MS = 6e4;
|
|
1374
|
+
var INSTABILITY_WARN_THRESHOLD = 5;
|
|
1356
1375
|
var GAP_FILL_WINDOW_MS = 2e3;
|
|
1357
1376
|
var MAX_BUFFERED_PER_CONVERSATION = 500;
|
|
1358
1377
|
var GAP_FILL_LIMIT = 200;
|
|
@@ -1394,6 +1413,12 @@ var RealtimeClient = class {
|
|
|
1394
1413
|
connectHandlers = /* @__PURE__ */ new Set();
|
|
1395
1414
|
disconnectHandlers = /* @__PURE__ */ new Set();
|
|
1396
1415
|
reconnectAttempts = 0;
|
|
1416
|
+
/** Clears reconnectAttempts once this connection proves itself stable. */
|
|
1417
|
+
stabilityTimer = null;
|
|
1418
|
+
/** Consecutive connections that died before STABLE_CONNECTION_MS. Drives
|
|
1419
|
+
* the operator warning only; backoff itself uses reconnectAttempts. */
|
|
1420
|
+
rapidReconnects = 0;
|
|
1421
|
+
lastConnectAt = null;
|
|
1397
1422
|
reconnectTimer = null;
|
|
1398
1423
|
helloAckTimer = null;
|
|
1399
1424
|
authenticated = false;
|
|
@@ -1506,7 +1531,7 @@ var RealtimeClient = class {
|
|
|
1506
1531
|
this.authenticated = true;
|
|
1507
1532
|
const caps = message.capabilities;
|
|
1508
1533
|
this.ackMode = Array.isArray(caps) && caps.includes("ack");
|
|
1509
|
-
this.
|
|
1534
|
+
this.startStabilityTimer();
|
|
1510
1535
|
if (this.helloAckTimer) {
|
|
1511
1536
|
clearTimeout(this.helloAckTimer);
|
|
1512
1537
|
this.helloAckTimer = null;
|
|
@@ -1541,6 +1566,8 @@ var RealtimeClient = class {
|
|
|
1541
1566
|
clearTimeout(this.helloAckTimer);
|
|
1542
1567
|
this.helloAckTimer = null;
|
|
1543
1568
|
}
|
|
1569
|
+
this.cancelStabilityTimer();
|
|
1570
|
+
this.noteConnectionEnded();
|
|
1544
1571
|
this.authenticated = false;
|
|
1545
1572
|
this.ackMode = false;
|
|
1546
1573
|
const selfClosedForHelloTimeout = this.helloTimeoutClose;
|
|
@@ -1701,6 +1728,52 @@ var RealtimeClient = class {
|
|
|
1701
1728
|
const state = this.orderStates.get(row.conversation_id);
|
|
1702
1729
|
return state !== void 0 && state.buffer.has(row.seq);
|
|
1703
1730
|
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Clear the reconnect backoff once this connection proves itself.
|
|
1733
|
+
*
|
|
1734
|
+
* Scheduled on `hello.ok`, cancelled on close. If it fires, the socket
|
|
1735
|
+
* has been up for STABLE_CONNECTION_MS and the next failure deserves to
|
|
1736
|
+
* start from the floor again. If it is cancelled, the connection died
|
|
1737
|
+
* young and the counter carries forward, so the delay keeps ramping
|
|
1738
|
+
* toward the cap.
|
|
1739
|
+
*/
|
|
1740
|
+
startStabilityTimer() {
|
|
1741
|
+
this.cancelStabilityTimer();
|
|
1742
|
+
this.lastConnectAt = Date.now();
|
|
1743
|
+
this.stabilityTimer = setTimeout(() => {
|
|
1744
|
+
this.stabilityTimer = null;
|
|
1745
|
+
if (this.disposed || !this.authenticated) return;
|
|
1746
|
+
this.reconnectAttempts = 0;
|
|
1747
|
+
this.rapidReconnects = 0;
|
|
1748
|
+
}, STABLE_CONNECTION_MS);
|
|
1749
|
+
this.stabilityTimer.unref?.();
|
|
1750
|
+
}
|
|
1751
|
+
cancelStabilityTimer() {
|
|
1752
|
+
if (this.stabilityTimer) {
|
|
1753
|
+
clearTimeout(this.stabilityTimer);
|
|
1754
|
+
this.stabilityTimer = null;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Track short-lived connections and warn once they form a pattern.
|
|
1759
|
+
* A flapping client looks healthy from the inside — every reconnect
|
|
1760
|
+
* succeeds — so without this the operator has no local signal at all.
|
|
1761
|
+
*/
|
|
1762
|
+
noteConnectionEnded() {
|
|
1763
|
+
const started = this.lastConnectAt;
|
|
1764
|
+
this.lastConnectAt = null;
|
|
1765
|
+
if (started === null) return;
|
|
1766
|
+
if (Date.now() - started >= RAPID_RECONNECT_MS) {
|
|
1767
|
+
this.rapidReconnects = 0;
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
this.rapidReconnects++;
|
|
1771
|
+
if (this.rapidReconnects === INSTABILITY_WARN_THRESHOLD) {
|
|
1772
|
+
console.warn(
|
|
1773
|
+
`[agentchat] realtime connection is unstable: ${this.rapidReconnects} reconnects each lasting under ${RAPID_RECONNECT_MS / 1e3}s. Backing off (next retry in up to ${this.options.maxReconnectInterval / 1e3}s). This usually means the network path or a local supervisor is dropping the socket, not an AgentChat outage.`
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1704
1777
|
scheduleReconnect() {
|
|
1705
1778
|
if (this.disposed) return;
|
|
1706
1779
|
if (!this.options.reconnect) return;
|
|
@@ -1799,6 +1872,7 @@ var RealtimeClient = class {
|
|
|
1799
1872
|
clearTimeout(this.helloAckTimer);
|
|
1800
1873
|
this.helloAckTimer = null;
|
|
1801
1874
|
}
|
|
1875
|
+
this.cancelStabilityTimer();
|
|
1802
1876
|
this.drainAllPendingForShutdown();
|
|
1803
1877
|
try {
|
|
1804
1878
|
this.ws?.close();
|
|
@@ -2173,106 +2247,6 @@ var RealtimeClient = class {
|
|
|
2173
2247
|
}
|
|
2174
2248
|
};
|
|
2175
2249
|
|
|
2176
|
-
// src/webhook-verify.ts
|
|
2177
|
-
var WebhookVerificationError = class extends Error {
|
|
2178
|
-
reason;
|
|
2179
|
-
constructor(reason, message) {
|
|
2180
|
-
super(message ?? reason);
|
|
2181
|
-
this.name = "WebhookVerificationError";
|
|
2182
|
-
this.reason = reason;
|
|
2183
|
-
}
|
|
2184
|
-
};
|
|
2185
|
-
async function verifyWebhook(options) {
|
|
2186
|
-
const { payload, signature, secret, toleranceSeconds = 300 } = options;
|
|
2187
|
-
const now2 = options.now ?? Date.now;
|
|
2188
|
-
if (!signature) {
|
|
2189
|
-
throw new WebhookVerificationError("missing_signature");
|
|
2190
|
-
}
|
|
2191
|
-
const parsed = parseSignatureHeader(signature);
|
|
2192
|
-
const bodyString = typeof payload === "string" ? payload : new TextDecoder().decode(payload);
|
|
2193
|
-
let expectedMessage;
|
|
2194
|
-
if (parsed.timestamp !== null) {
|
|
2195
|
-
if (toleranceSeconds > 0) {
|
|
2196
|
-
const ageSeconds = Math.abs(now2() / 1e3 - parsed.timestamp);
|
|
2197
|
-
if (ageSeconds > toleranceSeconds) {
|
|
2198
|
-
throw new WebhookVerificationError("timestamp_skew");
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
expectedMessage = `${parsed.timestamp}.${bodyString}`;
|
|
2202
|
-
} else {
|
|
2203
|
-
expectedMessage = bodyString;
|
|
2204
|
-
}
|
|
2205
|
-
const computed = await hmacSha256Hex(secret, expectedMessage);
|
|
2206
|
-
if (!constantTimeEqual(computed, parsed.digest)) {
|
|
2207
|
-
throw new WebhookVerificationError("bad_signature");
|
|
2208
|
-
}
|
|
2209
|
-
try {
|
|
2210
|
-
const json = JSON.parse(bodyString);
|
|
2211
|
-
return json;
|
|
2212
|
-
} catch {
|
|
2213
|
-
throw new WebhookVerificationError("malformed_payload");
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
function parseSignatureHeader(header) {
|
|
2217
|
-
const trimmed = header.trim();
|
|
2218
|
-
if (trimmed.includes("=")) {
|
|
2219
|
-
const parts = trimmed.split(",");
|
|
2220
|
-
let timestamp = null;
|
|
2221
|
-
let digest2 = null;
|
|
2222
|
-
for (const p of parts) {
|
|
2223
|
-
const idx = p.indexOf("=");
|
|
2224
|
-
if (idx <= 0) continue;
|
|
2225
|
-
const key = p.slice(0, idx).trim();
|
|
2226
|
-
const value = p.slice(idx + 1).trim();
|
|
2227
|
-
if (key === "t") {
|
|
2228
|
-
const n = Number(value);
|
|
2229
|
-
if (Number.isFinite(n)) timestamp = n;
|
|
2230
|
-
} else if (key === "v1") {
|
|
2231
|
-
digest2 = value.toLowerCase();
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
if (!digest2 || !/^[a-f0-9]+$/.test(digest2)) {
|
|
2235
|
-
throw new WebhookVerificationError("malformed_signature");
|
|
2236
|
-
}
|
|
2237
|
-
return { timestamp, digest: digest2 };
|
|
2238
|
-
}
|
|
2239
|
-
const digest = trimmed.toLowerCase();
|
|
2240
|
-
if (!/^[a-f0-9]+$/.test(digest)) {
|
|
2241
|
-
throw new WebhookVerificationError("malformed_signature");
|
|
2242
|
-
}
|
|
2243
|
-
return { timestamp: null, digest };
|
|
2244
|
-
}
|
|
2245
|
-
async function hmacSha256Hex(secret, message) {
|
|
2246
|
-
const subtle = globalThis.crypto?.subtle;
|
|
2247
|
-
if (!subtle) {
|
|
2248
|
-
throw new WebhookVerificationError(
|
|
2249
|
-
"bad_signature",
|
|
2250
|
-
"Web Crypto API not available in this runtime; webhook verification requires `globalThis.crypto.subtle`."
|
|
2251
|
-
);
|
|
2252
|
-
}
|
|
2253
|
-
const enc = new TextEncoder();
|
|
2254
|
-
const key = await subtle.importKey(
|
|
2255
|
-
"raw",
|
|
2256
|
-
enc.encode(secret),
|
|
2257
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
2258
|
-
false,
|
|
2259
|
-
["sign"]
|
|
2260
|
-
);
|
|
2261
|
-
const sig = await subtle.sign("HMAC", key, enc.encode(message));
|
|
2262
|
-
const bytes = new Uint8Array(sig);
|
|
2263
|
-
let hex = "";
|
|
2264
|
-
for (const b of bytes) hex += b.toString(16).padStart(2, "0");
|
|
2265
|
-
return hex;
|
|
2266
|
-
}
|
|
2267
|
-
function constantTimeEqual(a, b) {
|
|
2268
|
-
if (a.length !== b.length) return false;
|
|
2269
|
-
let mismatch = 0;
|
|
2270
|
-
for (let i = 0; i < a.length; i++) {
|
|
2271
|
-
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
2272
|
-
}
|
|
2273
|
-
return mismatch === 0;
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
2250
|
// src/render.ts
|
|
2277
2251
|
var SEC = 1e3;
|
|
2278
2252
|
var MIN = 60 * SEC;
|
|
@@ -2365,11 +2339,9 @@ exports.SuspendedError = SuspendedError;
|
|
|
2365
2339
|
exports.UnauthorizedError = UnauthorizedError;
|
|
2366
2340
|
exports.VERSION = VERSION;
|
|
2367
2341
|
exports.ValidationError = ValidationError;
|
|
2368
|
-
exports.WebhookVerificationError = WebhookVerificationError;
|
|
2369
2342
|
exports.createAgentChatError = createAgentChatError;
|
|
2370
2343
|
exports.paginate = paginate;
|
|
2371
2344
|
exports.parseRetryAfter = parseRetryAfter;
|
|
2372
2345
|
exports.renderMessageContext = renderMessageContext;
|
|
2373
|
-
exports.verifyWebhook = verifyWebhook;
|
|
2374
2346
|
//# sourceMappingURL=index.cjs.map
|
|
2375
2347
|
//# sourceMappingURL=index.cjs.map
|