chat 4.25.0 → 4.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -2881,6 +2881,21 @@ declare class Chat<TAdapters extends Record<string, Adapter> = Record<string, Ad
2881
2881
  */
2882
2882
  declare function fromFullStream(stream: AsyncIterable<unknown>): AsyncIterable<string | StreamChunk>;
2883
2883
 
2884
+ /**
2885
+ * Standalone JSON reviver for Chat SDK objects.
2886
+ *
2887
+ * Restores serialized Thread, Channel, and Message instances during
2888
+ * JSON.parse() without requiring a Chat instance. This is useful in
2889
+ * environments like Vercel Workflow functions where importing the full
2890
+ * Chat instance (with its adapter dependencies) is not possible.
2891
+ *
2892
+ * Thread instances created this way use lazy adapter resolution —
2893
+ * the adapter is looked up from the Chat singleton when first accessed,
2894
+ * so `chat.registerSingleton()` must be called before using thread
2895
+ * methods like `post()` (typically inside a "use step" function).
2896
+ */
2897
+ declare function reviver(_key: string, value: unknown): unknown;
2898
+
2884
2899
  interface StreamingMarkdownRendererOptions {
2885
2900
  /**
2886
2901
  * Wrap confirmed table blocks in code fences for append-only consumers that
@@ -3381,4 +3396,4 @@ declare const Select: SelectComponent;
3381
3396
  declare const SelectOption: SelectOptionComponent;
3382
3397
  declare const TextInput: TextInputComponent;
3383
3398
 
3384
- export { type ActionEvent, type ActionHandler, Actions, ActionsComponent, type Adapter, type AdapterPostableMessage, type AddTaskOptions, type AiAssistantMessage, type AiFilePart, type AiImagePart, type AiMessage, type AiMessagePart, type AiTextPart, type AiUserMessage, type AppHomeOpenedEvent, type AppHomeOpenedHandler, type AssistantContextChangedEvent, type AssistantContextChangedHandler, type AssistantThreadStartedEvent, type AssistantThreadStartedHandler, type Attachment, type Author, BaseFormatConverter, Button, ButtonComponent, Card, CardChild, CardComponent, CardElement, CardLink, CardLinkComponent, CardText, type Channel, ChannelImpl, type ChannelInfo, type ChannelVisibility, Chat, type ChatConfig, ChatElement, ChatError, type ChatInstance, type CompletePlanOptions, type ConcurrencyConfig, type ConcurrencyStrategy, ConsoleLogger, type CustomEmojiMap, DEFAULT_EMOJI_MAP, type DirectMessageHandler, Divider, DividerComponent, type Emoji, type EmojiFormats, type EmojiMapConfig, EmojiResolver, type EmojiValue, type EphemeralMessage, type FetchDirection, type FetchOptions, type FetchResult, Field, FieldComponent, Fields, FieldsComponent, type FileUpload, type FormatConverter, type FormattedContent, Image, ImageComponent, LinkButton, LinkButtonComponent, type LinkPreview, type ListThreadsOptions, type ListThreadsResult, type Lock, LockError, type LockScope, type LockScopeContext, type LogLevel, type Logger, type MarkdownConverter, type MarkdownTextChunk, type MemberJoinedChannelEvent, type MemberJoinedChannelHandler, type MentionHandler, Message, type MessageContext, type MessageData, type MessageHandler, MessageHistoryCache, type MessageHistoryConfig, type MessageMetadata, Modal, type ModalCloseEvent, type ModalCloseHandler, type ModalCloseResponse, ModalComponent, ModalElement, type ModalErrorsResponse, type ModalPushResponse, type ModalResponse, type ModalSubmitEvent, type ModalSubmitHandler, type ModalUpdateResponse, NotImplementedError, Plan, type PlanContent, type PlanModel, type PlanModelTask, type PlanTask, type PlanTaskStatus, type PlanUpdateChunk, type PostEphemeralOptions, type Postable, type PostableAst, type PostableCard, type PostableMarkdown, type PostableMessage, type PostableObject, type PostableObjectContext, type PostableRaw, type QueueEntry, RadioSelect, RadioSelectComponent, RateLimitError, type RawMessage, type ReactionEvent, type ReactionHandler, type ScheduledMessage, Section, SectionComponent, Select, SelectComponent, SelectOption, SelectOptionComponent, type SentMessage, type SerializedChannel, type SerializedMessage, type SerializedThread, type SlashCommandEvent, type SlashCommandHandler, type StartPlanOptions, type StateAdapter, type StreamChunk, type StreamEvent, type StreamOptions, StreamingMarkdownRenderer, type SubscribedMessageHandler, THREAD_STATE_TTL_MS, Table, type TaskUpdateChunk, TextComponent, TextInput, TextInputComponent, type Thread, ThreadImpl, type ThreadInfo, type ThreadSummary, type UpdateTaskInput, type WebhookOptions, type WellKnownEmoji, blockquote, cardChildToFallbackText, codeBlock, convertEmojiPlaceholders, createEmoji, defaultEmojiResolver, deriveChannelId, emoji, emphasis, fromFullStream, fromReactElement, fromReactModalElement, getEmoji, getNodeChildren, getNodeValue, inlineCode, isBlockquoteNode, isCardElement, isCodeNode, isDeleteNode, isEmphasisNode, isInlineCodeNode, isJSX, isLinkNode, isListItemNode, isListNode, isModalElement, isParagraphNode, isPostableObject, isStrongNode, isTableCellNode, isTableNode, isTableRowNode, isTextNode, link, markdownToPlainText, paragraph, parseMarkdown, root, strikethrough, stringifyMarkdown, strong, tableElementToAscii, tableToAscii, text, toAiMessages, toCardElement, toModalElement, toPlainText, walkAst };
3399
+ export { type ActionEvent, type ActionHandler, Actions, ActionsComponent, type Adapter, type AdapterPostableMessage, type AddTaskOptions, type AiAssistantMessage, type AiFilePart, type AiImagePart, type AiMessage, type AiMessagePart, type AiTextPart, type AiUserMessage, type AppHomeOpenedEvent, type AppHomeOpenedHandler, type AssistantContextChangedEvent, type AssistantContextChangedHandler, type AssistantThreadStartedEvent, type AssistantThreadStartedHandler, type Attachment, type Author, BaseFormatConverter, Button, ButtonComponent, Card, CardChild, CardComponent, CardElement, CardLink, CardLinkComponent, CardText, type Channel, ChannelImpl, type ChannelInfo, type ChannelVisibility, Chat, type ChatConfig, ChatElement, ChatError, type ChatInstance, type CompletePlanOptions, type ConcurrencyConfig, type ConcurrencyStrategy, ConsoleLogger, type CustomEmojiMap, DEFAULT_EMOJI_MAP, type DirectMessageHandler, Divider, DividerComponent, type Emoji, type EmojiFormats, type EmojiMapConfig, EmojiResolver, type EmojiValue, type EphemeralMessage, type FetchDirection, type FetchOptions, type FetchResult, Field, FieldComponent, Fields, FieldsComponent, type FileUpload, type FormatConverter, type FormattedContent, Image, ImageComponent, LinkButton, LinkButtonComponent, type LinkPreview, type ListThreadsOptions, type ListThreadsResult, type Lock, LockError, type LockScope, type LockScopeContext, type LogLevel, type Logger, type MarkdownConverter, type MarkdownTextChunk, type MemberJoinedChannelEvent, type MemberJoinedChannelHandler, type MentionHandler, Message, type MessageContext, type MessageData, type MessageHandler, MessageHistoryCache, type MessageHistoryConfig, type MessageMetadata, Modal, type ModalCloseEvent, type ModalCloseHandler, type ModalCloseResponse, ModalComponent, ModalElement, type ModalErrorsResponse, type ModalPushResponse, type ModalResponse, type ModalSubmitEvent, type ModalSubmitHandler, type ModalUpdateResponse, NotImplementedError, Plan, type PlanContent, type PlanModel, type PlanModelTask, type PlanTask, type PlanTaskStatus, type PlanUpdateChunk, type PostEphemeralOptions, type Postable, type PostableAst, type PostableCard, type PostableMarkdown, type PostableMessage, type PostableObject, type PostableObjectContext, type PostableRaw, type QueueEntry, RadioSelect, RadioSelectComponent, RateLimitError, type RawMessage, type ReactionEvent, type ReactionHandler, type ScheduledMessage, Section, SectionComponent, Select, SelectComponent, SelectOption, SelectOptionComponent, type SentMessage, type SerializedChannel, type SerializedMessage, type SerializedThread, type SlashCommandEvent, type SlashCommandHandler, type StartPlanOptions, type StateAdapter, type StreamChunk, type StreamEvent, type StreamOptions, StreamingMarkdownRenderer, type SubscribedMessageHandler, THREAD_STATE_TTL_MS, Table, type TaskUpdateChunk, TextComponent, TextInput, TextInputComponent, type Thread, ThreadImpl, type ThreadInfo, type ThreadSummary, type UpdateTaskInput, type WebhookOptions, type WellKnownEmoji, blockquote, cardChildToFallbackText, codeBlock, convertEmojiPlaceholders, createEmoji, defaultEmojiResolver, deriveChannelId, emoji, emphasis, fromFullStream, fromReactElement, fromReactModalElement, getEmoji, getNodeChildren, getNodeValue, inlineCode, isBlockquoteNode, isCardElement, isCodeNode, isDeleteNode, isEmphasisNode, isInlineCodeNode, isJSX, isLinkNode, isListItemNode, isListNode, isModalElement, isParagraphNode, isPostableObject, isStrongNode, isTableCellNode, isTableNode, isTableRowNode, isTextNode, link, markdownToPlainText, paragraph, parseMarkdown, reviver, root, strikethrough, stringifyMarkdown, strong, tableElementToAscii, tableToAscii, text, toAiMessages, toCardElement, toModalElement, toPlainText, walkAst };
package/dist/index.js CHANGED
@@ -729,7 +729,7 @@ var ChannelImpl = class _ChannelImpl {
729
729
  return {
730
730
  _type: "chat:Channel",
731
731
  id: this.id,
732
- adapterName: this.adapter.name,
732
+ adapterName: this._adapterName ?? this.adapter.name,
733
733
  channelVisibility: this.channelVisibility,
734
734
  isDM: this.isDM
735
735
  };
@@ -1541,7 +1541,7 @@ var ThreadImpl = class _ThreadImpl {
1541
1541
  return;
1542
1542
  }
1543
1543
  const content = renderer.render();
1544
- if (content !== lastEditContent) {
1544
+ if (content.trim() && content !== lastEditContent) {
1545
1545
  try {
1546
1546
  await this.adapter.editMessage(threadIdForEdits, msg.id, {
1547
1547
  markdown: content
@@ -1563,12 +1563,14 @@ var ThreadImpl = class _ThreadImpl {
1563
1563
  renderer.push(chunk);
1564
1564
  if (!msg) {
1565
1565
  const content = renderer.render();
1566
- msg = await this.adapter.postMessage(this.id, {
1567
- markdown: content
1568
- });
1569
- threadIdForEdits = msg.threadId || this.id;
1570
- lastEditContent = content;
1571
- scheduleNextEdit();
1566
+ if (content.trim()) {
1567
+ msg = await this.adapter.postMessage(this.id, {
1568
+ markdown: content
1569
+ });
1570
+ threadIdForEdits = msg.threadId || this.id;
1571
+ lastEditContent = content;
1572
+ scheduleNextEdit();
1573
+ }
1572
1574
  }
1573
1575
  }
1574
1576
  } finally {
@@ -1585,12 +1587,12 @@ var ThreadImpl = class _ThreadImpl {
1585
1587
  const finalContent = renderer.finish();
1586
1588
  if (!msg) {
1587
1589
  msg = await this.adapter.postMessage(this.id, {
1588
- markdown: accumulated
1590
+ markdown: accumulated.trim() ? accumulated : " "
1589
1591
  });
1590
1592
  threadIdForEdits = msg.threadId || this.id;
1591
1593
  lastEditContent = accumulated;
1592
1594
  }
1593
- if (finalContent !== lastEditContent) {
1595
+ if (finalContent.trim() && finalContent !== lastEditContent) {
1594
1596
  await this.adapter.editMessage(threadIdForEdits, msg.id, {
1595
1597
  markdown: accumulated
1596
1598
  });
@@ -1642,7 +1644,7 @@ var ThreadImpl = class _ThreadImpl {
1642
1644
  channelVisibility: this.channelVisibility,
1643
1645
  currentMessage: this._currentMessage?.toJSON(),
1644
1646
  isDM: this.isDM,
1645
- adapterName: this.adapter.name
1647
+ adapterName: this._adapterName ?? this.adapter.name
1646
1648
  };
1647
1649
  }
1648
1650
  /**
@@ -1832,6 +1834,23 @@ function extractMessageContent2(message) {
1832
1834
  throw new Error("Invalid PostableMessage format");
1833
1835
  }
1834
1836
 
1837
+ // src/reviver.ts
1838
+ function reviver(_key, value) {
1839
+ if (value && typeof value === "object" && "_type" in value) {
1840
+ const typed = value;
1841
+ if (typed._type === "chat:Thread") {
1842
+ return ThreadImpl.fromJSON(value);
1843
+ }
1844
+ if (typed._type === "chat:Channel") {
1845
+ return ChannelImpl.fromJSON(value);
1846
+ }
1847
+ if (typed._type === "chat:Message") {
1848
+ return Message.fromJSON(value);
1849
+ }
1850
+ }
1851
+ return value;
1852
+ }
1853
+
1835
1854
  // src/chat.ts
1836
1855
  var DEFAULT_LOCK_TTL_MS = 3e4;
1837
1856
  function sleep(ms) {
@@ -2248,21 +2267,7 @@ var Chat = class {
2248
2267
  */
2249
2268
  reviver() {
2250
2269
  this.registerSingleton();
2251
- return function reviver(_key, value) {
2252
- if (value && typeof value === "object" && "_type" in value) {
2253
- const typed = value;
2254
- if (typed._type === "chat:Thread") {
2255
- return ThreadImpl.fromJSON(value);
2256
- }
2257
- if (typed._type === "chat:Channel") {
2258
- return ChannelImpl.fromJSON(value);
2259
- }
2260
- if (typed._type === "chat:Message") {
2261
- return Message.fromJSON(value);
2262
- }
2263
- }
2264
- return value;
2265
- };
2270
+ return reviver;
2266
2271
  }
2267
2272
  // ChatInstance interface implementations
2268
2273
  /**
@@ -4071,6 +4076,7 @@ export {
4071
4076
  markdownToPlainText,
4072
4077
  paragraph,
4073
4078
  parseMarkdown,
4079
+ reviver,
4074
4080
  root,
4075
4081
  strikethrough,
4076
4082
  stringifyMarkdown,
package/docs/adapters.mdx CHANGED
@@ -34,7 +34,7 @@ Ready to build your own? Follow the [building](/docs/contributing/building) guid
34
34
  | Tables | ✅ Block Kit | ✅ GFM | ⚠️ ASCII | ✅ GFM | ⚠️ ASCII | ✅ GFM | ✅ GFM | ❌ |
35
35
  | Fields | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ Template variables |
36
36
  | Images in cards | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ |
37
- | Modals | ✅ | | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
37
+ | Modals | ✅ | | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
38
38
 
39
39
  ### Conversations
40
40
 
@@ -64,7 +64,7 @@ Ready to build your own? Follow the [building](/docs/contributing/building) guid
64
64
  ⚠️ indicates partial support — the feature works with limitations. See individual adapter pages for details.
65
65
  </Callout>
66
66
 
67
- ## How [adapters](/adapters) work
67
+ ## How adapters work
68
68
 
69
69
  Each adapter implements a standard interface that the `Chat` class uses to route events and send messages. When a webhook arrives:
70
70
 
@@ -73,7 +73,7 @@ Each adapter implements a standard interface that the `Chat` class uses to route
73
73
  3. Routes to your handlers via the `Chat` class
74
74
  4. Converts outgoing messages from markdown/AST/cards to the platform's native format
75
75
 
76
- ## Using multiple [adapters](/adapters)
76
+ ## Using multiple adapters
77
77
 
78
78
  Register multiple [adapters](/adapters) and your event handlers work across all of them:
79
79
 
@@ -95,6 +95,11 @@ Button({ id: "delete", label: "Delete", style: "danger", value: "item-123" })
95
95
  description: 'Optional payload sent with the action callback.',
96
96
  type: 'string',
97
97
  },
98
+ actionType: {
99
+ description: 'Hints to adapters like Teams that this button will open a modal via event.openModal().',
100
+ type: '"action" | "modal"',
101
+ default: '"action"',
102
+ },
98
103
  }}
99
104
  />
100
105
 
package/docs/api/chat.mdx CHANGED
@@ -475,3 +475,13 @@ Get a `JSON.parse` reviver that deserializes `Thread` and `Message` objects from
475
475
  const data = JSON.parse(payload, bot.reviver());
476
476
  await data.thread.post("Hello from workflow!");
477
477
  ```
478
+
479
+ There is also a standalone `reviver` export that works without a `Chat` instance. This is useful in Vercel Workflow functions where importing the full Chat instance (with its adapter dependencies) is not possible:
480
+
481
+ ```typescript
482
+ import { reviver } from "chat";
483
+
484
+ const data = JSON.parse(payload, reviver) as { thread: Thread; message: Message };
485
+ ```
486
+
487
+ The standalone reviver uses lazy adapter resolution - the adapter is looked up from the Chat singleton when first accessed. Call `chat.registerSingleton()` before using thread methods like `post()` (typically inside a `"use step"` function).
@@ -4,7 +4,7 @@ description: Modal form components for collecting user input.
4
4
  type: reference
5
5
  ---
6
6
 
7
- Modals display form dialogs that collect structured user input. Currently supported on Slack.
7
+ Modals display form dialogs that collect structured user input. Currently supported on Slack and Teams.
8
8
 
9
9
  ```typescript
10
10
  import { Modal, TextInput, Select, RadioSelect, SelectOption } from "chat";
package/docs/cards.mdx CHANGED
@@ -109,6 +109,12 @@ The `id` maps to your `onAction` handler. Optional `value` passes extra data:
109
109
  <Button id="report" value="bug">Report Bug</Button>
110
110
  ```
111
111
 
112
+ Set `actionType="modal"` to indicate the button opens a [modal](/docs/modals). The button still triggers your `onAction` handler, where you call `event.openModal()` — this prop tells adapters like Teams to wire up the button for dialog opening:
113
+
114
+ ```tsx title="lib/bot.tsx"
115
+ <Button id="open-feedback" actionType="modal">Give Feedback</Button>
116
+ ```
117
+
112
118
  ### CardLink
113
119
 
114
120
  Inline hyperlink rendered as text. Unlike `LinkButton` (which must be inside `Actions`), `CardLink` can be placed directly in a card alongside other content.
@@ -14,7 +14,7 @@ Learn the core patterns for handling incoming events and posting messages back t
14
14
  <Card title="Posting Messages" description="Different ways to render and send messages with thread.post()." href="/docs/posting-messages" />
15
15
  </Cards>
16
16
 
17
- ## [Adapters](/adapters)
17
+ ## Adapters
18
18
 
19
19
  Connect your bot to chat platforms and persist state across restarts.
20
20
 
@@ -98,14 +98,14 @@ export type ChatTurnPayload = {
98
98
 
99
99
  ## Create the durable session workflow
100
100
 
101
- The workflow receives the serialized thread and first message, restores them with `bot.reviver()`, and then keeps waiting for more turns through the hook.
101
+ The workflow receives the serialized thread and first message, restores them with `reviver`, and then keeps waiting for more turns through the hook.
102
102
 
103
103
  The important detail is that the workflow only orchestrates. Chat SDK side effects such as `post()`, `unsubscribe()`, and `setState()` stay inside step helpers:
104
104
 
105
105
  ```typescript title="workflows/durable-chat-session.ts" lineNumbers
106
- import { Message, type Thread } from "chat";
106
+ import { Message, reviver, type Thread } from "chat";
107
107
  import { createHook, getWorkflowMetadata } from "workflow";
108
- import { bot, type ThreadState } from "@/lib/bot";
108
+ import type { ThreadState } from "@/lib/bot";
109
109
  import type { ChatTurnPayload } from "@/workflows/chat-turn-hook";
110
110
 
111
111
  async function postAssistantMessage(
@@ -114,6 +114,7 @@ async function postAssistantMessage(
114
114
  ) {
115
115
  "use step";
116
116
 
117
+ const { bot } = await import("@/lib/bot");
117
118
  await bot.initialize();
118
119
  await thread.post(text);
119
120
  }
@@ -121,6 +122,7 @@ async function postAssistantMessage(
121
122
  async function closeSession(thread: Thread<ThreadState>) {
122
123
  "use step";
123
124
 
125
+ const { bot } = await import("@/lib/bot");
124
126
  await bot.initialize();
125
127
  await thread.post("Session closed.");
126
128
  await thread.unsubscribe();
@@ -154,7 +156,7 @@ export async function durableChatSession(payload: string) {
154
156
  "use workflow";
155
157
 
156
158
  const { workflowRunId } = getWorkflowMetadata();
157
- const { thread, message } = JSON.parse(payload, bot.reviver()) as {
159
+ const { thread, message } = JSON.parse(payload, reviver) as {
158
160
  thread: Thread<ThreadState>;
159
161
  message: Message;
160
162
  };
@@ -186,11 +188,15 @@ export async function durableChatSession(payload: string) {
186
188
  The `using` keyword requires TypeScript 5.2+ with `"lib": ["esnext.disposable"]` in your `tsconfig.json`. If you are on an older version, call `hook.dispose()` manually when the session ends.
187
189
  </Callout>
188
190
 
191
+ <Callout type="warn">
192
+ Do not import `bot` at the top level of a workflow file. Adapter packages depend on Node.js modules that are not available in the workflow sandbox. Use the standalone `reviver` for deserialization and import `bot` dynamically inside `"use step"` functions where Node.js modules are available.
193
+ </Callout>
194
+
189
195
  This is the core integration:
190
196
 
191
197
  - `thread.toJSON()` and `message.toJSON()` cross the workflow boundary safely
192
- - `bot.reviver()` restores real Chat SDK objects inside the workflow
193
- - `bot.registerSingleton()` lets Workflow deserialize `Thread` objects again inside step functions
198
+ - `reviver` restores real Chat SDK objects inside the workflow without pulling in adapter dependencies
199
+ - `registerSingleton()` is called in `lib/bot.ts` and the singleton is available inside step functions when `bot` is dynamically imported
194
200
  - `createHook<ChatTurnPayload>({ token: workflowRunId })` makes the workflow run itself the session identifier
195
201
  - `runTurn()`, `postAssistantMessage()`, and `closeSession()` are steps, so adapter and state side effects stay outside the workflow sandbox
196
202
 
@@ -328,4 +334,4 @@ From here you can add:
328
334
  - [Handling Events](/docs/handling-events) — Mentions, subscribed messages, and routing behavior
329
335
  - [Streaming](/docs/streaming) — Stream AI SDK responses directly to chat platforms
330
336
  - [Thread API](/docs/api/thread) — `thread.toJSON()`, `thread.setState()`, and other thread primitives
331
- - [Chat API](/docs/api/chat) — `bot.reviver()`, initialization, and webhook access
337
+ - [Chat API](/docs/api/chat) — `reviver`, initialization, and webhook access
package/docs/modals.mdx CHANGED
@@ -6,7 +6,7 @@ prerequisites:
6
6
  - /docs/actions
7
7
  ---
8
8
 
9
- Modals open form dialogs in response to button clicks or [slash commands](/docs/slash-commands). They support text inputs, dropdowns, radio buttons, and server-side validation. Currently supported on Slack.
9
+ Modals open form dialogs in response to button clicks or [slash commands](/docs/slash-commands). They support text inputs, dropdowns, radio buttons, and server-side validation. Currently supported on Slack and Teams.
10
10
 
11
11
  ## Open a modal
12
12
 
package/docs/state.mdx CHANGED
@@ -8,7 +8,7 @@ prerequisites:
8
8
 
9
9
  State adapters handle persistent storage for thread subscriptions, distributed locks (to prevent duplicate processing), and caching. You must provide a state adapter when creating a `Chat` instance. Browse all available state adapters on the [Adapters](/adapters) page.
10
10
 
11
- ## What state [adapters](/adapters) manage
11
+ ## What state adapters manage
12
12
 
13
13
  ### Thread subscriptions
14
14
 
package/docs/usage.mdx CHANGED
@@ -39,7 +39,7 @@ bot.onNewMention(async (thread) => {
39
39
 
40
40
  Each adapter factory auto-detects credentials from environment variables (`SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET`, `REDIS_URL`, etc.), so you can get started with zero config. Pass explicit values to override.
41
41
 
42
- ## Multiple [adapters](/adapters)
42
+ ## Multiple adapters
43
43
 
44
44
  Register multiple [adapters](/adapters) to deploy your bot across platforms simultaneously:
45
45
 
@@ -76,7 +76,7 @@ Your event handlers work identically across all registered adapters — the SDK
76
76
  | `fallbackStreamingPlaceholderText` | `string \| null` | `"..."` | Placeholder text while streaming starts. Set to `null` to skip |
77
77
  | `onLockConflict` | `'drop' \| 'force' \| (threadId, message) => 'drop' \| 'force'` | `"drop"` | Behavior when a thread lock is already held. `'force'` releases the existing lock and re-acquires it, enabling interrupt/steerability for long-running handlers |
78
78
 
79
- ## Accessing [adapters](/adapters)
79
+ ## Accessing adapters
80
80
 
81
81
  Use `getAdapter` to access platform-specific APIs when you need functionality beyond the unified interface:
82
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chat",
3
- "version": "4.25.0",
3
+ "version": "4.26.0",
4
4
  "description": "Unified chat abstraction for Slack, Teams, Google Chat, and Discord",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,222 +0,0 @@
1
- ---
2
- title: WhatsApp
3
- description: Configure the WhatsApp adapter for the WhatsApp Business Cloud API.
4
- type: integration
5
- prerequisites:
6
- - /docs/getting-started
7
- ---
8
-
9
- ## Installation
10
-
11
- ```sh title="Terminal"
12
- pnpm add @chat-adapter/whatsapp
13
- ```
14
-
15
- ## Usage
16
-
17
- The adapter auto-detects `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_APP_SECRET`, `WHATSAPP_PHONE_NUMBER_ID`, and `WHATSAPP_VERIFY_TOKEN` from environment variables:
18
-
19
- ```typescript title="lib/bot.ts" lineNumbers
20
- import { Chat } from "chat";
21
- import { createWhatsAppAdapter } from "@chat-adapter/whatsapp";
22
-
23
- const bot = new Chat({
24
- userName: "mybot",
25
- adapters: {
26
- whatsapp: createWhatsAppAdapter(),
27
- },
28
- });
29
-
30
- bot.onNewMention(async (thread, message) => {
31
- await thread.post(`You said: ${message.text}`);
32
- });
33
- ```
34
-
35
- Since all WhatsApp conversations are 1:1 DMs, every incoming message is treated as a mention.
36
-
37
- ## Webhook route
38
-
39
- WhatsApp uses two webhook mechanisms:
40
-
41
- 1. **Verification handshake** (GET) — Meta sends a `hub.verify_token` challenge that must match your `WHATSAPP_VERIFY_TOKEN`.
42
- 2. **Event delivery** (POST) — incoming messages, reactions, and interactive responses, verified via `X-Hub-Signature-256`.
43
-
44
- Both are handled by the same `handleWebhook` method:
45
-
46
- ```typescript title="app/api/webhooks/whatsapp/route.ts" lineNumbers
47
- import { bot } from "@/lib/bot";
48
-
49
- export async function GET(request: Request): Promise<Response> {
50
- return bot.webhooks.whatsapp(request);
51
- }
52
-
53
- export async function POST(request: Request): Promise<Response> {
54
- return bot.webhooks.whatsapp(request);
55
- }
56
- ```
57
-
58
- ## Meta app setup
59
-
60
- ### 1. Create a Meta app
61
-
62
- 1. Go to [developers.facebook.com/apps](https://developers.facebook.com/apps)
63
- 2. Click **Create App** and select **Business** type
64
- 3. Give it a name and click **Create App**
65
-
66
- ### 2. Add WhatsApp product
67
-
68
- 1. In the app dashboard, find **WhatsApp** and click **Set Up**
69
- 2. This creates a test phone number and sandbox environment
70
-
71
- ### 3. Get credentials
72
-
73
- From the app dashboard:
74
-
75
- | Credential | Where to find it |
76
- |---|---|
77
- | **Access Token** | WhatsApp > API Setup > Temporary access token (or create a System User token for production) |
78
- | **Phone Number ID** | WhatsApp > API Setup > Phone number ID |
79
- | **App Secret** | Settings > Basic > App Secret |
80
- | **Verify Token** | You define this — any secret string you choose |
81
-
82
- ### 4. Configure webhooks
83
-
84
- 1. Go to **WhatsApp** > **Configuration** in your app dashboard
85
- 2. Click **Edit** next to Webhook URL
86
- 3. Set **Callback URL** to `https://your-domain.com/api/webhooks/whatsapp`
87
- 4. Set **Verify token** to the same value as your `WHATSAPP_VERIFY_TOKEN`
88
- 5. Click **Verify and Save**
89
- 6. Subscribe to the **messages** webhook field
90
-
91
- ### 5. Production access
92
-
93
- For production use:
94
-
95
- 1. Add a real phone number under **WhatsApp** > **API Setup**
96
- 2. Create a **System User** in Meta Business Suite for a permanent access token
97
- 3. Complete Meta's **App Review** process for the `whatsapp_business_messaging` permission
98
-
99
- ## Interactive messages
100
-
101
- Card elements are automatically converted to WhatsApp interactive messages:
102
-
103
- - **3 or fewer buttons** — rendered as WhatsApp reply buttons (title max 20 characters)
104
- - **More than 3 buttons** — falls back to formatted text
105
-
106
- ```typescript title="lib/bot.ts" lineNumbers
107
- import { Card, Actions, Button, Body, BodyText } from "chat";
108
-
109
- bot.onNewMention(async (thread) => {
110
- await thread.post(
111
- <Card>
112
- <Body>
113
- <BodyText>How can I help?</BodyText>
114
- </Body>
115
- <Actions>
116
- <Button id="help" value="help">Get Help</Button>
117
- <Button id="status" value="status">Check Status</Button>
118
- </Actions>
119
- </Card>
120
- );
121
- });
122
- ```
123
-
124
- ## Media attachments
125
-
126
- Incoming media messages (images, documents, audio, video, voice, stickers) are exposed as attachments with a lazy `fetchData()` function. Media is downloaded in two steps via the Graph API — first fetching the media URL, then downloading the binary data.
127
-
128
- ```typescript title="lib/bot.ts" lineNumbers
129
- bot.onNewMention(async (thread, message) => {
130
- for (const attachment of message.attachments) {
131
- if (attachment.fetchData) {
132
- const data = await attachment.fetchData();
133
- // Process the media buffer...
134
- }
135
- }
136
- });
137
- ```
138
-
139
- ## 24-hour messaging window
140
-
141
- WhatsApp enforces a [24-hour customer service window](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages#customer-service-windows). You can only send free-form messages to a user within 24 hours of their last message. After that, you must use approved **message templates**.
142
-
143
- ## Configuration
144
-
145
- All options are auto-detected from environment variables when not provided.
146
-
147
- | Option | Required | Description |
148
- |--------|----------|-------------|
149
- | `accessToken` | No* | Meta access token. Auto-detected from `WHATSAPP_ACCESS_TOKEN` |
150
- | `appSecret` | No* | App secret for webhook signature verification. Auto-detected from `WHATSAPP_APP_SECRET` |
151
- | `phoneNumberId` | No* | Bot's phone number ID. Auto-detected from `WHATSAPP_PHONE_NUMBER_ID` |
152
- | `verifyToken` | No* | Secret for webhook verification handshake. Auto-detected from `WHATSAPP_VERIFY_TOKEN` |
153
- | `apiVersion` | No | Graph API version (defaults to `v21.0`) |
154
- | `userName` | No | Bot username. Auto-detected from `WHATSAPP_BOT_USERNAME` or defaults to `whatsapp-bot` |
155
- | `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) |
156
-
157
- *All four credentials are required — either via config or environment variables.
158
-
159
- ## Environment variables
160
-
161
- ```bash title=".env.local"
162
- WHATSAPP_ACCESS_TOKEN=EAAx... # Meta access token
163
- WHATSAPP_APP_SECRET=abc123... # App secret for signature verification
164
- WHATSAPP_PHONE_NUMBER_ID=1234567890 # Phone number ID from Meta dashboard
165
- WHATSAPP_VERIFY_TOKEN=my-secret # Your chosen webhook verify token
166
- ```
167
-
168
- ## Features
169
-
170
- | Feature | Supported |
171
- |---------|-----------|
172
- | Mentions | N/A (all messages are DMs) |
173
- | Reactions (add/remove) | Yes |
174
- | Cards | Interactive messages (max 3 buttons) / text fallback |
175
- | Modals | No |
176
- | Streaming | No |
177
- | DMs | Yes (all conversations) |
178
- | Ephemeral messages | No |
179
- | File uploads | Receive only |
180
- | Typing indicator | Yes |
181
- | Message history | No |
182
- | Edit message | No (throws) |
183
- | Delete message | No (throws) |
184
-
185
- ## Thread ID format
186
-
187
- ```
188
- whatsapp:{phoneNumberId}:{userWaId}
189
- ```
190
-
191
- Example: `whatsapp:1234567890:15551234567`
192
-
193
- ## Notes
194
-
195
- - All WhatsApp conversations are 1:1 DMs between the business phone number and the user, so every message sets `isMention: true`.
196
- - `editMessage()` and `deleteMessage()` throw errors — WhatsApp does not support these operations.
197
- - `fetchMessages()` returns empty results — WhatsApp does not provide a message history API.
198
- - Messages exceeding 4096 characters are automatically split at paragraph or line boundaries.
199
- - Webhook signatures are verified using HMAC-SHA256 with `timingSafeEqual` for timing-attack resistance.
200
-
201
- ## Troubleshooting
202
-
203
- ### Webhook verification failing
204
-
205
- - Verify `WHATSAPP_VERIFY_TOKEN` matches what you set in the Meta dashboard
206
- - Ensure both GET and POST routes are configured for the webhook URL
207
-
208
- ### "Invalid signature" error
209
-
210
- - Check that `WHATSAPP_APP_SECRET` is correct (find it under Settings > Basic in your Meta app)
211
- - Ensure the raw request body is not parsed before verification
212
-
213
- ### Bot not responding
214
-
215
- - Confirm the **messages** webhook field is subscribed in Meta dashboard
216
- - Check that you're within the 24-hour messaging window (or using template messages)
217
- - Verify the phone number ID matches your configured `WHATSAPP_PHONE_NUMBER_ID`
218
-
219
- ### Media downloads failing
220
-
221
- - Ensure your access token has the required permissions
222
- - Check that the media hasn't expired (WhatsApp media URLs are temporary)