Commit Graph

577 Commits

Author SHA1 Message Date
github-actions[bot] 7a1798bdfd chore(release): version packages (#841)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @chat-adapter/shared@4.39.0

### Minor Changes

- 153bd96: guard Messenger attachment downloads against SSRF and
oversized responses

`downloadAttachment` in `@chat-adapter/shared` accepts an optional
`hosts` allowlist that restricts downloads, including redirect targets,
to the given hosts and their subdomains. The Messenger adapter uses it
to download attachment media only from Meta's `fbsbx.com` and
`fbcdn.net` hosts, with the shared SSRF guard, 25 MB size cap, and 30
second timeout. External fallback and link-share URLs are rejected
before any network request.

- bb92688: secure anonymous attachment downloads against SSRF and
oversized responses

`@chat-adapter/shared` gains `downloadAttachment`, a guarded downloader
that refuses private and internal addresses (as URL literals, through
DNS resolution, and after redirects), decodes compressed responses, caps
the body size at 25 MB, and bounds the whole download with a 30 second
timeout. All of these are configurable, including the transport for
proxied deployments.

The Teams adapter uses it for anonymous attachment downloads. HTTPS
attachments on any public host keep working, plain-HTTP URLs are
refused, and the Bot Framework Emulator's loopback connector now uses
bot authentication so local development keeps working.

- e71bfea: Add `normalizeCodeFences` to `@chat-adapter/shared`: a
code-fence normalizer for platforms whose triple-backtick fences treat
the text after the opening fence as code rather than a CommonMark info
string. Only paired fences become code blocks (unpaired fences, fences
inside inline code, and fences on quoted lines stay literal text), text
following a closing fence cannot be promoted to a block construct, and
per-segment callbacks keep text-level rewrites out of code content.

The WhatsApp adapter now uses it when parsing incoming messages: the
first line of a code block is preserved in message text and formatted
content, and bold/strikethrough rewriting no longer corrupts fenced
code.

- b6fa24c: guard attachment downloads across the remaining adapters

Slack, Discord, and WhatsApp attachment downloads now go through the
shared guarded downloader: private and internal addresses are refused
(as URL literals, through DNS resolution, and after redirects),
responses are capped at 25 MB, and downloads time out after 30 seconds.
Slack sends the bot token only on hops to trusted Slack origins, and
WhatsApp keeps its access token on Meta's media hosts and the configured
Graph origin. Telegram enforces the same size cap and timeout with the
Web Fetch API so downloads keep working in runtimes like Cloudflare
Workers.

`downloadAttachment` in `@chat-adapter/shared` now resolves `headers`
per hop (pass a function to control what each redirect target receives),
forwards the resolved headers to custom transports, and accepts an
`onResponse` hook to reject unexpected final responses before the body
is read.

### Patch Changes

- Updated dependencies [2ce2be0]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [5b538f6]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
  - chat@4.39.0
## @chat-adapter/slack@4.39.0

### Minor Changes

- 2ce2be0: Add Slack Agent Sessions lifecycle support, native stop
cancellation through `thread.signal`, automatic session titles, and
session stop/title-change events while preserving the legacy
`assistant_view` compatibility path.

### Patch Changes

- 864d922: Keep alert attachment content on normalized Slack messages.
Attachments that aren't link unfurls now contribute their pretext, title
(linked to `title_link` when present, with the URL also surfaced in
`message.links`), text, and fields instead of being dropped; `fallback`
fills in when nothing else on the attachment carries content. Matching
how Slack renders these fields, they are treated as plain text unless
listed in the attachment's `mrkdwn_in` array, so literal `*`, `_`, and
backticks in alert text survive normalization. Tables inside attachment
blocks now stay adjacent to their attachment's text.

Because attachment content is part of `message.text`, mention detection
and `onMessage` pattern handlers see it too: an attachment that quotes
the bot's mention routes to `onNewMention`, and patterns match alert
text. Handlers that should ignore other integrations' alerts can check
`message.author.isBot`.

- 7c26965: prevent attachment downloads from sending credentials to
untrusted hosts
- e71bfea: Preserve the first line of incoming Slack code blocks when
extracting message text and formatted content.

Only paired triple-backtick fences become code blocks: an unpaired
fence, a fence inside inline code or a `<…>` token, and a fence on a
quoted line all stay literal text, matching how Slack renders them. Bold
and strikethrough rewriting no longer touches fenced code content, and
text following a closing fence can no longer turn into a blockquote,
heading, or list.

- b6fa24c: guard attachment downloads across the remaining adapters

Slack, Discord, and WhatsApp attachment downloads now go through the
shared guarded downloader: private and internal addresses are refused
(as URL literals, through DNS resolution, and after redirects),
responses are capped at 25 MB, and downloads time out after 30 seconds.
Slack sends the bot token only on hops to trusted Slack origins, and
WhatsApp keeps its access token on Meta's media hosts and the configured
Graph origin. Telegram enforces the same size cap and timeout with the
Web Fetch API so downloads keep working in runtimes like Cloudflare
Workers.

`downloadAttachment` in `@chat-adapter/shared` now resolves `headers`
per hop (pass a function to control what each redirect target receives),
forwards the resolved headers to custom transports, and accepts an
`onResponse` hook to reject unexpected final responses before the body
is read.

- Updated dependencies [2ce2be0]
- Updated dependencies [153bd96]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [bb92688]
- Updated dependencies [5b538f6]
- Updated dependencies [e71bfea]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
- Updated dependencies [b6fa24c]
  - chat@4.39.0
  - @chat-adapter/shared@4.39.0
## @chat-adapter/telegram@4.39.0

### Minor Changes

- c4a359e: require webhook verification by default with an explicit
unverified opt-in
- 26a06ca: Add `mentionOnReply`: when enabled, a reply to one of the
bot's own messages reports `isMention`, so a bot in a group keeps the
conversation going without the handle being repeated. Off by default, so
existing mention-only bots are unaffected, and readable from
`TELEGRAM_MENTION_ON_REPLY`. Implicit forum-topic replies and the bot's
own echoed messages never count, and polling mode now retries the
startup `getMe` lazily so a transient outage cannot leave mention
detection disabled.
- d5ebec1: Implement `reply` in the Telegram adapter so `Thread.reply()`
threads the answer to its target instead of throwing
`NotImplementedError`. The reference travels as Bot API
`reply_parameters` and covers text, rich messages, documents,
attachments and media groups; `allow_sending_without_reply` keeps
delivery working when the target has been deleted. Malformed reply
target ids are rejected before anything is sent, and a rich-message
gateway that rejects `reply_parameters` falls back to a regular threaded
send.
- a18e792: Describe the message kinds Telegram sends with no text and no
file. A shared location, venue, contact, poll, dice, game, invoice or
story used to arrive as an empty message: the payload carried the
content, but a handler reading `text` saw nothing. Each now gets a short
literal description (`📍 55.75, 37.61`, `👤 Ada Lovelace +1555…`, `📊 Lunch
or dinner?`), and the structured payload stays on the raw message for
anyone who needs the numbers.
- a0ba986: Parse stickers and animations. A sticker used to arrive as an
empty message, since it carries no text, and an animation (Telegram GIF)
was dropped entirely. A sticker now reports the emoji it stands for as
its text (falling back to the sticker set name, then to "sticker") plus
an attachment matching its real format: an image for a still WebP
sticker, a video for a WebM one, a file for a Lottie (TGS) one. An
animation arrives as a single video attachment; the redundant `document`
field Telegram sets alongside it for backward compatibility is no longer
reported as a second attachment.

### Patch Changes

- eddcd7e: Return Telegram file downloads as portable ArrayBuffer data
while preserving Buffer support in the shared attachment contract.
- b6fa24c: guard attachment downloads across the remaining adapters

Slack, Discord, and WhatsApp attachment downloads now go through the
shared guarded downloader: private and internal addresses are refused
(as URL literals, through DNS resolution, and after redirects),
responses are capped at 25 MB, and downloads time out after 30 seconds.
Slack sends the bot token only on hops to trusted Slack origins, and
WhatsApp keeps its access token on Meta's media hosts and the configured
Graph origin. Telegram enforces the same size cap and timeout with the
Web Fetch API so downloads keep working in runtimes like Cloudflare
Workers.

`downloadAttachment` in `@chat-adapter/shared` now resolves `headers`
per hop (pass a function to control what each redirect target receives),
forwards the resolved headers to custom transports, and accepts an
`onResponse` hook to reject unexpected final responses before the body
is read.

- Updated dependencies [2ce2be0]
- Updated dependencies [153bd96]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [bb92688]
- Updated dependencies [5b538f6]
- Updated dependencies [e71bfea]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
- Updated dependencies [b6fa24c]
  - chat@4.39.0
  - @chat-adapter/shared@4.39.0
## @chat-adapter/twilio@4.39.0

### Minor Changes

- 75cadbf: feat(twilio): add RCS support with rich cards, button
actions, and location sharing

Extends the Twilio adapter with full RCS support: inbound button tap
routing via `processAction`, location share parsing, Content API
integration for rich outbound cards with SMS fallback, and channel
metadata detection. Cards sent to RCS-capable senders (Messaging Service
or `rcs:` address) are automatically rendered as Twilio Content
templates with embedded SMS fallback variants.

Existing deployments keep their thread ids: plain SMS threads stay keyed
by phone number even when the number belongs to a Messaging Service, and
`openDM` still prefers `phoneNumber` over `messagingServiceSid`. Only
taps of buttons rendered by Chat SDK become actions; foreign button taps
that carry a body keep arriving as messages.

### Patch Changes

- 28bc776: isolate Twilio message processing locks by conversation
- Updated dependencies [2ce2be0]
- Updated dependencies [153bd96]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [bb92688]
- Updated dependencies [5b538f6]
- Updated dependencies [e71bfea]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
- Updated dependencies [b6fa24c]
  - chat@4.39.0
  - @chat-adapter/shared@4.39.0
## chat@4.39.0

### Minor Changes

- 2ce2be0: Add Slack Agent Sessions lifecycle support, native stop
cancellation through `thread.signal`, automatic session titles, and
session stop/title-change events while preserving the legacy
`assistant_view` compatibility path.
- 169788b: Introduce the unified History API (`bot.history`) with user,
thread, and channel scopes.

`bot.history.user` replaces `bot.transcripts` for cross-platform
per-user message persistence. The API surface is identical — migrate by
changing the `transcripts` config key to `history.user` and updating
call sites from `bot.transcripts.*` to `bot.history.user.*`.
`bot.transcripts` remains available as a deprecated alias.

`bot.history.thread` and `bot.history.channel` expose promise-based
helpers for per-thread and per-channel message access, aligned with the
existing `thread.messages` and `channel.threads()` iterators. Reads
delegate to the adapter; adapters that persist history in the SDK-side
store (`persistThreadHistory: true`) are served from that cache. An
unregistered adapter prefix or an unsupported capability throws instead
of returning an empty result.

The new `toPromptEntries` helper converts `history.user.list()` entries
into `{ role, content }` messages ready for LLM prompts.

The `TranscriptEntry` type is deprecated in favour of `HistoryEntry`.
Both are exported from `chat`.

- 5b538f6: Keep thread locks alive while message handlers run so queue,
burst, and debounce strategies remain serialized beyond the lock TTL.
Renewal is capped by the new `concurrency.maxLockLifetimeMs` option
(default 10 minutes) so a hung handler cannot block a thread forever.
When the heartbeat detects that lock ownership was lost, the queue drain
and debounce loops stop instead of competing with the new lock holder,
and the debounce loop now keeps draining messages that arrive while a
handler is running instead of stranding them until the next webhook.

### Patch Changes

- 16ea171: preserve adapter-returned thread ids when editing channel
messages
- eddcd7e: Return Telegram file downloads as portable ArrayBuffer data
while preserving Buffer support in the shared attachment contract.
- 929878b: Allow JSX link buttons to include an explicit action ID.
- 500b7e6: enforce the conversation scope on write tools and stop
trusting client-supplied message history in the web adapter

`createChatTools` now runs the same scope guard on write tools that read
tools already used, so a thread or channel id the model supplies that
resolves outside the scoped conversation is rejected before the write
executes. `sendDirectMessage` targets a user id rather than a
conversation and stays gated by approval alone.

The web adapter no longer treats the request body's `messages` array as
a source of conversation state. Only the latest user message is
consumed, and tool parts are stripped from it so a browser cannot inject
forged tool-call or approval state. Text, file, and custom data parts
pass through unchanged; a message left with no parts after stripping is
rejected with HTTP 400. Prior turns come from the state adapter when
`persistMessageHistory` is enabled.
## @chat-adapter/discord@4.39.0

### Patch Changes

- c4f709f: fix edits, deletes, and reactions on Discord thread starter
messages

Operations on a thread's starter message now try the thread first and
fall back to the parent channel when Discord reports the message as
unknown. Threads on a text channel keep their starter message in the
parent channel, so those operations used to fail; forum and media posts
keep theirs in the thread and are unaffected.

Note that deleting a text-channel thread's starter message now deletes
the message, which Discord cascades into deleting the thread.

- b6fa24c: guard attachment downloads across the remaining adapters

Slack, Discord, and WhatsApp attachment downloads now go through the
shared guarded downloader: private and internal addresses are refused
(as URL literals, through DNS resolution, and after redirects),
responses are capped at 25 MB, and downloads time out after 30 seconds.
Slack sends the bot token only on hops to trusted Slack origins, and
WhatsApp keeps its access token on Meta's media hosts and the configured
Graph origin. Telegram enforces the same size cap and timeout with the
Web Fetch API so downloads keep working in runtimes like Cloudflare
Workers.

`downloadAttachment` in `@chat-adapter/shared` now resolves `headers`
per hop (pass a function to control what each redirect target receives),
forwards the resolved headers to custom transports, and accepts an
`onResponse` hook to reject unexpected final responses before the body
is read.

- Updated dependencies [2ce2be0]
- Updated dependencies [153bd96]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [bb92688]
- Updated dependencies [5b538f6]
- Updated dependencies [e71bfea]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
- Updated dependencies [b6fa24c]
  - chat@4.39.0
  - @chat-adapter/shared@4.39.0
## @chat-adapter/gchat@4.39.0

### Patch Changes

- Updated dependencies [2ce2be0]
- Updated dependencies [153bd96]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [bb92688]
- Updated dependencies [5b538f6]
- Updated dependencies [e71bfea]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
- Updated dependencies [b6fa24c]
  - chat@4.39.0
  - @chat-adapter/shared@4.39.0
## @chat-adapter/github@4.39.0

### Patch Changes

- Updated dependencies [2ce2be0]
- Updated dependencies [153bd96]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [bb92688]
- Updated dependencies [5b538f6]
- Updated dependencies [e71bfea]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
- Updated dependencies [b6fa24c]
  - chat@4.39.0
  - @chat-adapter/shared@4.39.0
## @chat-adapter/instagram@4.39.0

### Patch Changes

- Updated dependencies [2ce2be0]
- Updated dependencies [153bd96]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [bb92688]
- Updated dependencies [5b538f6]
- Updated dependencies [e71bfea]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
- Updated dependencies [b6fa24c]
  - chat@4.39.0
  - @chat-adapter/shared@4.39.0
## @chat-adapter/linear@4.39.0

### Patch Changes

- Updated dependencies [2ce2be0]
- Updated dependencies [153bd96]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [bb92688]
- Updated dependencies [5b538f6]
- Updated dependencies [e71bfea]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
- Updated dependencies [b6fa24c]
  - chat@4.39.0
  - @chat-adapter/shared@4.39.0
## @chat-adapter/messenger@4.39.0

### Patch Changes

- 153bd96: guard Messenger attachment downloads against SSRF and
oversized responses

`downloadAttachment` in `@chat-adapter/shared` accepts an optional
`hosts` allowlist that restricts downloads, including redirect targets,
to the given hosts and their subdomains. The Messenger adapter uses it
to download attachment media only from Meta's `fbsbx.com` and
`fbcdn.net` hosts, with the shared SSRF guard, 25 MB size cap, and 30
second timeout. External fallback and link-share URLs are rejected
before any network request.

- Updated dependencies [2ce2be0]
- Updated dependencies [153bd96]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [bb92688]
- Updated dependencies [5b538f6]
- Updated dependencies [e71bfea]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
- Updated dependencies [b6fa24c]
  - chat@4.39.0
  - @chat-adapter/shared@4.39.0
## @chat-adapter/notion@4.39.0

### Patch Changes

- Updated dependencies [2ce2be0]
- Updated dependencies [153bd96]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [bb92688]
- Updated dependencies [5b538f6]
- Updated dependencies [e71bfea]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
- Updated dependencies [b6fa24c]
  - chat@4.39.0
  - @chat-adapter/shared@4.39.0
## @chat-adapter/teams@4.39.0

### Patch Changes

- 63997ac: hydrate live Teams sender email without requiring Microsoft
Graph permissions
- bb92688: secure anonymous attachment downloads against SSRF and
oversized responses

`@chat-adapter/shared` gains `downloadAttachment`, a guarded downloader
that refuses private and internal addresses (as URL literals, through
DNS resolution, and after redirects), decodes compressed responses, caps
the body size at 25 MB, and bounds the whole download with a 30 second
timeout. All of these are configurable, including the transport for
proxied deployments.

The Teams adapter uses it for anonymous attachment downloads. HTTPS
attachments on any public host keep working, plain-HTTP URLs are
refused, and the Bot Framework Emulator's loopback connector now uses
bot authentication so local development keeps working.

- Updated dependencies [2ce2be0]
- Updated dependencies [153bd96]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [bb92688]
- Updated dependencies [5b538f6]
- Updated dependencies [e71bfea]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
- Updated dependencies [b6fa24c]
  - chat@4.39.0
  - @chat-adapter/shared@4.39.0
## @chat-adapter/web@4.39.0

### Patch Changes

- 500b7e6: enforce the conversation scope on write tools and stop
trusting client-supplied message history in the web adapter

`createChatTools` now runs the same scope guard on write tools that read
tools already used, so a thread or channel id the model supplies that
resolves outside the scoped conversation is rejected before the write
executes. `sendDirectMessage` targets a user id rather than a
conversation and stays gated by approval alone.

The web adapter no longer treats the request body's `messages` array as
a source of conversation state. Only the latest user message is
consumed, and tool parts are stripped from it so a browser cannot inject
forged tool-call or approval state. Text, file, and custom data parts
pass through unchanged; a message left with no parts after stripping is
rejected with HTTP 400. Prior turns come from the state adapter when
`persistMessageHistory` is enabled.

- Updated dependencies [2ce2be0]
- Updated dependencies [153bd96]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [bb92688]
- Updated dependencies [5b538f6]
- Updated dependencies [e71bfea]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
- Updated dependencies [b6fa24c]
  - chat@4.39.0
  - @chat-adapter/shared@4.39.0
## @chat-adapter/whatsapp@4.39.0

### Patch Changes

- 3e6e866: support business-scoped user IDs for inbound and outbound
WhatsApp messages
- e71bfea: Add `normalizeCodeFences` to `@chat-adapter/shared`: a
code-fence normalizer for platforms whose triple-backtick fences treat
the text after the opening fence as code rather than a CommonMark info
string. Only paired fences become code blocks (unpaired fences, fences
inside inline code, and fences on quoted lines stay literal text), text
following a closing fence cannot be promoted to a block construct, and
per-segment callbacks keep text-level rewrites out of code content.

The WhatsApp adapter now uses it when parsing incoming messages: the
first line of a code block is preserved in message text and formatted
content, and bold/strikethrough rewriting no longer corrupts fenced
code.

- 7c26965: prevent attachment downloads from sending credentials to
untrusted hosts
- b6fa24c: guard attachment downloads across the remaining adapters

Slack, Discord, and WhatsApp attachment downloads now go through the
shared guarded downloader: private and internal addresses are refused
(as URL literals, through DNS resolution, and after redirects),
responses are capped at 25 MB, and downloads time out after 30 seconds.
Slack sends the bot token only on hops to trusted Slack origins, and
WhatsApp keeps its access token on Meta's media hosts and the configured
Graph origin. Telegram enforces the same size cap and timeout with the
Web Fetch API so downloads keep working in runtimes like Cloudflare
Workers.

`downloadAttachment` in `@chat-adapter/shared` now resolves `headers`
per hop (pass a function to control what each redirect target receives),
forwards the resolved headers to custom transports, and accepts an
`onResponse` hook to reject unexpected final responses before the body
is read.

- Updated dependencies [2ce2be0]
- Updated dependencies [153bd96]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [bb92688]
- Updated dependencies [5b538f6]
- Updated dependencies [e71bfea]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
- Updated dependencies [b6fa24c]
  - chat@4.39.0
  - @chat-adapter/shared@4.39.0
## @chat-adapter/x@4.39.0

### Patch Changes

- Updated dependencies [2ce2be0]
- Updated dependencies [153bd96]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [bb92688]
- Updated dependencies [5b538f6]
- Updated dependencies [e71bfea]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
- Updated dependencies [b6fa24c]
  - chat@4.39.0
  - @chat-adapter/shared@4.39.0
## @chat-adapter/state-ioredis@4.39.0

### Patch Changes

- Updated dependencies [2ce2be0]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [5b538f6]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
  - chat@4.39.0
## @chat-adapter/state-memory@4.39.0

### Patch Changes

- Updated dependencies [2ce2be0]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [5b538f6]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
  - chat@4.39.0
## @chat-adapter/state-pg@4.39.0

### Patch Changes

- Updated dependencies [2ce2be0]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [5b538f6]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
  - chat@4.39.0
## @chat-adapter/state-redis@4.39.0

### Patch Changes

- Updated dependencies [2ce2be0]
- Updated dependencies [16ea171]
- Updated dependencies [169788b]
- Updated dependencies [eddcd7e]
- Updated dependencies [5b538f6]
- Updated dependencies [929878b]
- Updated dependencies [500b7e6]
  - chat@4.39.0
## @chat-adapter/tests@4.39.0

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-28 20:37:34 +10:00
OSS Polar Bear 75cadbf9aa feat(twilio): add RCS support for interactive inbound and rich outbound (#590)
Extend the Twilio adapter with RCS webhook parsing, Content API
integration, and card-to-template mapping with SMS fallback.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 19:57:44 +10:00
OSS Polar Bear 169788b65a feat(chat): introduce unified History API with user, thread, and chan… (#592)
Adds `bot.history` as the canonical entry point for message history,
with three scopes: `user`, `thread`, and `channel`. `bot.transcripts`
stays as a deprecated alias, so nothing breaks.

## Why

History access was spread across `bot.transcripts`, `thread.messages` /
`thread.allMessages`, and per-adapter calls. `bot.history` puts the
promise-based read paths in one place, and the AI tools
(`fetchMessages`, `fetchChannelMessages`, `listThreads`) now route
through it.

## User scope

Cross-platform per-user persistence, identical in surface to
`bot.transcripts`:

```typescript
const bot = new Chat({
  adapters: { slack, telegram },
  state,
  history: {
    user: {
      identity: ({ author }) => author.email ?? null,
      retention: "30d",
      maxPerUser: 200,
    },
  },
});

await bot.history.user.append(thread, message);
const entries = await bot.history.user.list({ userKey, limit: 20 });
await bot.history.user.delete({ userKey });
```

The new `toPromptEntries` helper turns those entries into `{ role,
content }` messages for an LLM call:

```typescript
import { toPromptEntries } from "chat";

const entries = await bot.history.user.list({ userKey });
const { text } = await generateText({
  model,
  messages: toPromptEntries(entries),
});
```

## Thread scope

Single-page reads and an auto-paginating generator:

```typescript
// One page, newest messages by default
const { messages, nextCursor } = await bot.history.thread.list(thread.id, {
  limit: 20,
});

// Everything, oldest first, pagination handled for you
for await (const msg of bot.history.thread.collect(thread.id, { limit: 50 })) {
  console.log(msg.text);
}
```

## Channel scope

```typescript
// Top-level channel messages (not thread replies)
const { messages } = await bot.history.channel.listMessages("slack:C123", {
  limit: 20,
});

// Thread listings
const { threads } = await bot.history.channel.listThreads("slack:C123");

// Threads together with a page of messages each
const result = await bot.history.channel.listThreadsWithMessages("slack:C123", {
  maxThreads: 5,
  messagesPerThread: 10,
});
```

## Semantics

The read paths are strict about where data comes from:

- The adapter named in the ID prefix must be registered. A typo'd or
unknown prefix throws instead of reading as an empty conversation.
- The SDK-side `ThreadHistoryCache` only serves adapters that persist
history there (`persistThreadHistory: true`, e.g. Telegram, WhatsApp).
For every other adapter the platform response is authoritative, so an
empty page is a real empty page, and a `cursor` always returns the
adapter's response as-is.
- Cache reads honor the same windows as adapter reads: backward
(default) gives the newest N, forward the oldest N, and `collect()`
yields the oldest N on both paths.
- `channel.listMessages` throws a capability error on adapters without
`fetchChannelMessages` (persisting adapters are served from the
channel-keyed cache instead), and `listThreadsWithMessages` fetches
per-thread pages through `history.thread.list` a few threads at a time
to stay inside platform rate limits.

## Migration

```typescript
// Before
const bot = new Chat({
  identity: ({ author }) => author.email ?? null,
  transcripts: { retention: "30d", maxPerUser: 200 },
});
await bot.transcripts.append(thread, msg);

// After
const bot = new Chat({
  history: {
    user: {
      identity: ({ author }) => author.email ?? null,
      retention: "30d",
      maxPerUser: 200,
    },
  },
});
await bot.history.user.append(thread, msg);
```

You can migrate one field at a time: when both `history.user` and the
legacy `transcripts` block are set they merge, with `history.user`
winning field by field, so settings left on `transcripts` keep applying
until you move them. `TranscriptEntry` is deprecated in favour of
`HistoryEntry` (also exported as `UserHistoryEntry`); all deprecated
names keep working in the current major version.

## Included

- New `packages/chat/src/history/` module with unit tests for every
scope
- AI tools rewired to `bot.history`, keeping their scope guards
- The nextjs example uses the new APIs throughout, with Thread History
and Channel History test buttons that exercise every scope
- Docs: `/docs/history` guide, `/docs/api/history` reference,
deprecation callouts on the transcripts pages
- Changeset (`minor` for `chat`)

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 18:47:26 +10:00
Yevanchen 5b538f6f21 fix(chat): keep thread locks alive during long handlers (#821)
- renew a held thread or channel lock every 10 seconds while a locking
concurrency strategy is running
- stop the heartbeat before releasing the lock, and handle extension
failures without unhandled rejections
- add regression coverage proving `queue`, `burst`, and `debounce`
remain serialized when a handler exceeds the 30-second lock TTL
- keep the existing short TTL, so a crashed process still releases its
lock automatically

Mosoo Agents hit this with Chat SDK's Telegram adapter while waiting on
long-running Codex Agent handlers. Once a handler crossed 30 seconds, a
later Telegram message could acquire an expired channel lock and run
concurrently on the same conversation.

Fixes #685.

---------

Signed-off-by: Yevanchen <cyefan2@gmail.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 17:39:26 +10:00
Amit Vijapur 864d922204 fix(slack): keep alert attachment content on normalized messages (#846)
Fixes #608.

Slack integrations such as Sentry, PagerDuty and GitHub carry their real
payload in an attachment's `title`, `text` and `fields`. The adapter
only read attachments to build link-unfurl previews, so that content
reached nowhere on the normalized `Message`: `msg.text` held only the
top-level one-line summary, and every consumer inherited the gap,
including `thread.messages`, `toAiMessages` and the `chat/ai`
`fetchMessages` tool.

Non-unfurl attachment content is now folded into the text before the AST
is assembled, so both `formatted` and the derived plain text carry it.
This follows the same approach as #817, which preserved pasted tables.

Three decisions worth calling out for review:

- Link unfurls stay excluded, on the same grounds their blocks already
are via `isForeignAttachment`: the content is not the message author's.
- `fallback` is used only when the attachment has neither its own fields
nor blocks. It is otherwise a plain-text stand-in that duplicates
content rendered elsewhere, and including it unconditionally would
inject strings like `[no preview available]` into messages that already
carry table blocks.
- Mentions inside attachment content are resolved on the async path
only, matching how table cells are already handled in `resolvedContent`.

`fields` was missing from the `SlackEvent["attachments"]` type and has
been added.

## Test plan

Three tests added in `packages/adapter-slack/src/index.test.ts`,
covering alert content (title, text and fields), the `fallback`-only
case, and exclusion of unfurl attachments. The alert test asserts both
the sync `parseMessage` and async `parseSlackMessage` paths, following
the existing table-attachment test.

- `vitest run src/index.test.ts` in `packages/adapter-slack`: 419
passed.
- Reverting only the source change and keeping the new tests makes two
of the three fail with the reported symptom (`expected 'New alert' to be
'New alert\n\nTypeError: cannot read p…'`), confirming they exercise the
bug rather than the implementation.
- `tsc --noEmit` clean, `ultracite check` clean on the changed files.
- `pnpm validate` passes except `create-chat-sdk#test`, which fails
identically on `main` at 294b595 in my environment with `Cannot find
package 'chat/adapters'`. That looks like local workspace resolution
rather than anything in this change.

---------

Signed-off-by: Amit Vijapur <vijapuramit@gmail.com>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 16:35:59 +10:00
Akazawa Daisuke e71bfead52 fix(slack): preserve first line of incoming code blocks (#843)
## Summary

fixes #842

normalizes incoming Slack triple-backtick code fences before parsing
them as CommonMark

Slack treats text immediately after an opening fence as code content,
while CommonMark treats it as the fence's info string; putting Slack
fences on their own lines preserves the first code line in both
`message.text` and `message.formatted`

the normalization also separates fences from surrounding text so inline
Slack code blocks are parsed as fenced code blocks instead of regular
Markdown text

## Test plan

- added format-level regression coverage for code starting immediately
after an opening fence and for fences adjacent to surrounding text
- added converter-level assertions for the parsed code node and
extracted plain text
- ran the focused Slack format and Markdown test suites
- ran `pnpm validate`

## Checklist

- [x] All commits are signed and verified
- [x] All commits are signed off for the DCO (`git commit -s`)
- [x] `pnpm validate` passes
- [x] Changeset added (or N/A — see
[CONTRIBUTING.md](./CONTRIBUTING.md))
- [x] Documentation updated (or N/A)

---------

Signed-off-by: akkadaska <akkadaska@gmail.com>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 15:09:03 +10:00
I'm Groot 🌳 a18e79224e feat(telegram): describe locations, contacts, polls and dice (#836)
Telegram sends several message kinds with neither text nor a file. They
reached the handler as empty messages: the content was in the payload,
but anything reading `text` saw nothing and could not tell an empty
delivery from a shared location.

A location, venue, contact, poll, dice, game, invoice and story now each
produce a short literal description, in the same place a sticker
produces its emoji:

```
📍 55.75, 37.61
📍 Central Library, 12 Main St
👤 Ada Lovelace +15551234567
📊 Lunch or dinner?
🎲 4
🎮 Corsairs
🧾 Yearly plan — 49.99 USD
📖 Story
```

The wording stays minimal and the structured payload is untouched on the
raw message, so a handler that wants the coordinates or the poll options
still has them.

Two Bot API quirks shape the implementation:

- A venue message also carries a top-level `location` field for backward
compatibility, so the venue check runs first. Otherwise every venue
would render as bare coordinates.
- An invoice's `total_amount` is in the currency's smallest unit, and
the exponent varies per currency
([currencies.json](https://core.telegram.org/bots/payments/currencies.json)):
JPY and Telegram Stars count whole units, BHD, IQD and JOD use three
decimals, everything else two.

`sample-messages.md` gains fixtures for the new kinds, including the
venue with its co-set location and a contact without a `last_name`.

Signed-off-by: grootbro <vadim@ravefox.dev>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 14:16:21 +10:00
I'm Groot 🌳 a0ba986827 feat(telegram): parse stickers and animations (#835)
Based on #834.

A sticker carries no text, so it reached the handler as an empty message
and looked like a delivery that had lost its body. An animation — the
MP4 Telegram sends for a GIF — was not declared on the message type and
was dropped on the floor.

A sticker now reports the emoji it stands for as the message text, plus
an image attachment typed by its real format: WebP for a still one, WebM
for a video sticker, TGS for a Lottie one. An animation arrives as a
video attachment alongside the other media types.

---------

Signed-off-by: grootbro <vadim@ravefox.dev>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 13:03:27 +10:00
I'm Groot 🌳 26a06ca51d feat(telegram): treat a reply to the bot as a mention (#834)
Based on #833.

In a group a bot only sees messages that address it, and people address
a bot by replying to it as often as by typing its handle. The adapter
reported `isMention` for the handle but not for the reply, so a bot went
quiet the moment the conversation moved to replies.

`mentionOnReply` turns that on. **Off by default** — the flag changes
which messages report `isMention`, and a bot that deliberately answers
only explicit mentions should keep the stricter behaviour. It also reads
`TELEGRAM_MENTION_ON_REPLY`, so a deployment can set it without code,
and the key is declared in the adapters catalog.

The check runs before the empty-text guard, so a reply carrying only a
photo or a document counts too.

---------

Signed-off-by: grootbro <vadim@ravefox.dev>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 12:15:32 +10:00
I'm Groot 🌳 d5ebec127b feat(telegram): implement native message replies (#833)
`Thread.reply()` throws `NotImplementedError` on Telegram: the adapter
has no `reply` method, even though the Bot API threads an answer to its
question with `reply_parameters`.

`postMessage` takes an optional reply target and passes it to every send
path — text, rich messages, documents, attachments and both media group
variants — and `reply()` delegates to it, the same shape the WhatsApp
adapter uses for this contract. The target is decoded through the
existing `decodeCompositeMessageId`, so a target from another chat is
rejected exactly as an edit would be.

`allow_sending_without_reply` is set: a deleted target degrades to an
unthreaded message instead of failing the send.

Three tests cover it: the reference lands on a reply, a plain
`postMessage` stays unthreaded, and a target from another chat is
refused.

---------

Signed-off-by: grootbro <vadim@ravefox.dev>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 01:16:28 +10:00
Mahdi Jaafar 500b7e6d2c fix(web): prevent tool approval bypass via client-supplied messages array (#857)
Hardens two trust boundaries reported against the framework: the web
adapter derived conversation state from the client-supplied
`body.messages` array, and the AI SDK write tools skipped the
conversation scope check that read tools already enforced.

## Web adapter: client-supplied messages

`handleWebhook` previously accepted the full `useChat` `messages` array
from the browser. A client could forge tool-call and approval parts in
it, and handlers reading `message.raw` would see that forged state as if
the server had produced it.

The adapter now:

- consumes only the latest user message and ignores the rest of the
array
- strips tool parts from that message, so forged tool-call or approval
state never reaches handlers; text, file, and custom `data-*` parts pass
through to `message.raw` unchanged
- returns 400 when nothing usable remains after stripping
- no longer passes `originalMessages` to `createUIMessageStream`
(nothing registers `onFinish`, so it was never consumed; prior turns
come from the state adapter via `persistMessageHistory`, never from the
request body)

## AI SDK tools: scope on writes

`createChatTools` now runs the same scope guard on write tools that read
tools already used. A thread or channel id the model supplies that
resolves outside the scoped conversation is rejected before the write
executes. The guard is threaded through each tool factory
(`ToolOptions.guard`) rather than wrapped around `execute`, so it is
typed against each tool's input schema and a future tool can't ship
unguarded.

`sendDirectMessage` targets a user id rather than a conversation, so the
guard has nothing to check it against; it stays gated by approval, and
the docs now say so explicitly.

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 00:01:50 +10:00
Ben Sabic b6fa24c68f fix(adapters): guard attachment downloads across slack, discord, telegram, and whatsapp (#865)
Follows up on #850, #856, and #859 by adopting the shared guarded
downloader (`downloadAttachment` in `@chat-adapter/shared`) in the
remaining adapters that fetch attachment bytes from event-supplied URLs.

- Slack, Discord, and WhatsApp attachment downloads now refuse private
and internal addresses (as URL literals, through DNS resolution, and
after redirects), cap responses at 25 MB, and time out after 30 seconds.
- Slack sends the bot token only on hops to trusted Slack origins, so a
redirect can never carry it to another host, and keeps the
HTML-login-page detection. A protected `createFileTransport()` override
routes downloads through a proxy.
- WhatsApp keeps its access token on Meta's media hosts, and the
configured Graph origin via the hosts allowlist; `downloadMedia()`
accepts a custom transport.
- Telegram keeps downloads on the Web Fetch API because a downstream
Cloudflare Workers consumer depends on portability (#828), enforcing the
same 25 MB cap and 30-second timeout with web streams.
- `downloadAttachment` now resolves `headers` per hop (function form
decides what each redirect target receives), forwards the resolved
headers to custom transports, and accepts an `onResponse` hook that can
reject a final response before its body is read.
- Adds "Inbound attachments" docs sections for all four adapters.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-27 13:05:06 +10:00
Ben Sabic 2ce2be008f feat(slack): add Agent Sessions lifecycle and native stop (#862)
Migrates Slack's `agent_view` integration to the Agent Sessions
lifecycle while preserving the legacy `assistant_view` compatibility
path.

- Adds `agents.sessions.setStatus` and `agents.sessions.rename` support
for processing, active, suspended, and closed sessions.
- Handles `agent_session_stopped` without taking the message lock,
clears Slack's processing state, and dispatches `onAgentSessionStopped`.
- Adds cross-process turn cancellation through the configured state
adapter and exposes the active turn as `thread.signal`.
- Handles `agent_session_title_changed` and automatically titles new
agent conversations from their root message, with a configurable
resolver.
- Propagates `session_status` through native stream completion and
supports suspended human-in-the-loop turns.
- Updates Slack manifests, examples, API docs, fixtures, and migration
guidance for the February 2027 `assistant_view` retirement.

Configure the Agent messaging experience and optional title resolver:

```ts
const slack = createSlackAdapter({
  agentView: true,
  sessionTitle: ({ text }) => text.split("\n", 1)[0]?.slice(0, 80) ?? null,
});
```

Pass the thread signal into model generation so Slack's native stop
button cancels upstream work as well as message delivery:

```ts
bot.onDirectMessage(async (thread, message) => {
  await thread.startTyping();

  const result = await agent.stream({
    prompt: message.text,
    abortSignal: thread.signal,
  });

  await thread.post(result.fullStream);
});
```

React to session lifecycle events:

```ts
bot.onAgentSessionStopped(async (event) => {
  await releaseExternalResources(event.threadId);
});

bot.onAgentSessionTitleChanged(async (event) => {
  await syncTitle(event.threadId, event.title);
});
```

Leave a stream suspended when the agent needs user input or approval:

```ts
await thread.post(
  new StreamingPlan(result.fullStream, {
    sessionStatus: "suspended",
  })
);
```

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-27 10:03:14 +10:00
josh 153bd9640d fix(messenger): guard attachment downloads (#856)
## summary

- restrict Messenger attachment downloads to Meta's `fbsbx.com` and
`fbcdn.net` hosts while preserving external URLs on `attachment.url`
- reject untrusted URLs before connecting using HTTPS validation,
connection-bound DNS checks, manual redirect validation, timeouts, and
streamed size limits
- move the guarded downloader into `@chat-adapter/shared` and keep the
Teams implementation behaviorally equivalent
- normalize malformed redirect locations and other download failures as
typed `NetworkError` values
- document the inbound attachment policy for Messenger
- stacked on #850 and should merge after it

## test plan

- verified valid Meta image, audio, video, and file CDN hosts remain
downloadable
- verified external hosts, private addresses, malformed URLs, unsafe
ports, trailing dots, and suffix attacks are rejected
- verified mixed private and public DNS results fail closed
- verified redirects are revalidated and malformed or external
destinations are rejected
- verified declared and streamed size limits and stalled body timeouts
- ran workspace build, affected package tests and typechecks,
integration checks, Knip, Ultracite, and diff validation

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-25 21:30:21 +10:00
josh bb926884a2 fix(teams): secure attachment downloads (#850)
## summary

- restrict anonymous attachment downloads to current Microsoft 365
SharePoint and OneDrive for Business hosts
- reject internal addresses using connection-bound DNS validation
- revalidate every redirect and disable connection reuse outside the
guarded transport
- enforce a 25 MB streaming response limit and a 15 second request
timeout
- preserve connector-origin bot authentication and the protected custom
fetch override
- document the default anonymous download policy

## test plan

- verify trusted Microsoft 365 attachment hosts remain supported
- verify HTTP, custom ports, lookalike domains, trailing-dot hosts, and
generic off-origin URLs are rejected
- verify private IPv4, encoded IPv4, bracketed IPv6, and mixed DNS
results are rejected
- verify redirects are revalidated before another request
- verify oversized streamed responses are stopped
- verify activity parsing and attachment rehydration use the guarded
transport
- run Teams tests, typecheck, formatting, and production builds

---------

Signed-off-by: dancer <josh@afterima.ge>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-25 21:09:08 +10:00
Max eddcd7e46b fix(telegram): return portable file data (#828)
## Summary

Telegram file downloads already receive their bytes from the Web Fetch
API as an `ArrayBuffer`, but the adapter immediately converts them with
`Buffer.from(...)` before returning. That conversion is unnecessary for
consumers that accept web-standard binary data, and it throws when the
Node `Buffer` global is unavailable. The [Fetch
standard](https://fetch.spec.whatwg.org/#dom-body-arraybuffer) defines
`Response.arrayBuffer()` as returning an `ArrayBuffer`; Cloudflare
Workers exposes the [Fetch API
natively](https://developers.cloudflare.com/workers/runtime-apis/fetch/),
while `Buffer` belongs to its [Node.js compatibility
surface](https://developers.cloudflare.com/workers/runtime-apis/nodejs/buffer/).

This change returns the fetched `ArrayBuffer` directly from Telegram.
The shared `Attachment.fetchData` and protected Telegram method use
`Buffer | ArrayBuffer` so existing adapters and subclasses that return
`Buffer` remain source-compatible. The two consumers of that contract
now accept the portable value: `chat/ai` passes `ArrayBuffer` directly
to the AI SDK, and the X adapter normalizes either type at its
Buffer-based upload boundary. The public file documentation and patch
changesets are updated with the same contract.

The downstream evidence is a pnpm patch in the private Calories
Cloudflare Workers consumer at
`patches/@chat-adapter__telegram@4.36.0.patch`. Its portability hunk
changes `downloadFile` from `Promise<Buffer>` to `Promise<ArrayBuffer>`
and changes `Buffer.from(await response.arrayBuffer())` to
`response.arrayBuffer()`; the other Telegram hunks in that patch are
already upstream and are intentionally excluded here.

## Test plan

- `pnpm validate`
- `pnpm --filter @chat-adapter/telegram test` (269 tests)
- `pnpm --filter @chat-adapter/telegram typecheck`
- `pnpm --filter chat test` (1,131 tests)
- `pnpm --filter chat typecheck`
- `pnpm --filter @chat-adapter/x test` (222 tests)
- `pnpm --filter @chat-adapter/x typecheck`
- Added a regression test that removes the global `Buffer`, exercises
Telegram's mocked `getFile` and file-fetch path, and asserts the
returned bytes are an `ArrayBuffer`.

The runtime proof is limited to the isolated download seam under Node
with `Buffer` removed. This PR does not claim a deployed
no-compatibility Cloudflare Worker or a live Telegram end-to-end
request.

## Checklist

- [x] All commits are signed and verified
- [x] All commits are signed off for the DCO (`git commit -s`)
- [x] `pnpm validate` passes
- [x] Changeset added (or N/A — see
[CONTRIBUTING.md](./CONTRIBUTING.md))
- [x] Documentation updated (or N/A)

---------

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-08-25 21:03:46 +10:00
Max 63997acaa8 fix(teams): hydrate incoming users without Graph (#860)
## Summary

Changes live incoming Teams author hydration to
`ctx.api.conversations.getMemberById`, so the normal path no longer
requires Microsoft Graph's `User.Read.All` permission or tenant admin
consent. Explicit `getUser()` lookups remain Graph-backed.

## Test Plan

- `pnpm --filter @chat-adapter/teams test` (264 passed)
- `pnpm --filter @chat-adapter/teams exec vitest run src/index.test.ts
-t 'incoming sender email'` (8 passed)
- `pnpm --filter @chat-adapter/teams typecheck`
- `pnpm --filter @chat-adapter/teams... build`
- `pnpm check`
- `git diff --check`
- built and packed `@chat-adapter/teams`; inspected the artifact for
both the Connector lookup and preserved Graph lookup

The regression tests assert the exact activity conversation and sender
IDs, Graph isolation on Connector success and failure, cache behavior,
the missing-AAD fallback, and the DM path. A live Microsoft Teams tenant
was not available for runtime verification.

## Checklist

- [x] All commits are signed and verified
- [x] All commits are signed off for the DCO (`git commit -s`)
- [ ] `pnpm validate` passes
- [x] Changeset added (or N/A — see
[CONTRIBUTING.md](./CONTRIBUTING.md))
- [x] Documentation updated (or N/A)

---------

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-08-25 20:45:27 +10:00
josh 28bc776858 fix(twilio): isolate message locks by conversation (#849)
## summary

- use thread-scoped locking so separate Twilio conversations no longer
contend for the same sender lock
- preserve the existing Twilio channel ID format and channel behavior
- add regression coverage proving concurrent recipients process
independently

Signed-off-by: dancer <josh@afterima.ge>
2026-08-25 20:04:31 +10:00
josh 16ea171e68 fix(chat): preserve thread id when editing channel messages (#848)
## summary

- preserve adapter-returned thread ids on messages returned by channel
message edits
- prevent edited messages from falling back to the parent channel id
- add regression coverage for adapters that return a thread id when
posting a channel message

Signed-off-by: dancer <josh@afterima.ge>
2026-08-25 20:03:24 +10:00
josh c4a359e7e9 fix(telegram): require webhook verification by default (#858)
## summary

- require `secretToken` when Telegram resolves to webhook mode
- reject unverified messages and callback queries before dispatch
- add `allowUnverifiedWebhooks` as an explicit escape hatch for local
fixtures or trusted upstream verification
- preserve polling without requiring webhook credentials
- deduplicate every accepted webhook update
- update adapter docs, configuration metadata, and integration fixtures

---------

Signed-off-by: dancer <josh@afterima.ge>
2026-08-25 20:02:02 +10:00
josh 7c269653ef fix(adapters): restrict attachment credentials to trusted hosts (#859)
## summary

- only send Slack bearer tokens to trusted Slack file origins or the
configured API origin
- fetch external Slack file URLs without authentication, including
rehydrated attachments
- reject WhatsApp media URLs outside trusted Meta CDN hosts or the
configured Graph origin before sending credentials
- apply the Slack policy to both the adapter and lower-level API
primitive

## test plan

- verify trusted Slack file URLs receive bearer authentication
- verify external and malformed Slack URLs receive no authentication
- verify rehydrated cross-tenant Slack attachments do not resolve or
send installation tokens
- verify trusted Meta CDN and configured Graph URLs remain downloadable
- verify untrusted, malformed, HTTP, and host-confusion WhatsApp URLs
are rejected

Signed-off-by: dancer <josh@afterima.ge>
2026-08-25 19:58:30 +10:00
josh c4f709fe93 fix(discord): route thread starter message operations correctly (#815)
## summary

- route edits, deletes, and reactions on text channel thread starters
through the parent channel
- retry against the thread channel only when Discord returns unknown
message, preserving forum and media post behavior
- preserve Discord API error codes so fallback behavior is limited to
error `10008`
- add regression coverage for parent routing, forum fallback, and
unrelated Discord errors
- fixes #809

---------

Signed-off-by: dancer <josh@afterima.ge>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-20 22:28:26 +10:00
josh 3e6e866a0c fix(whatsapp): support business-scoped user ids (#818)
- support phone-based IDs, BSUIDs, parent BSUIDs, and username-only
webhook payloads
- preserve existing thread IDs by storing identity aliases and outbound
routing details in the configured state adapter
- send replies using `to`, `recipient`, or both according to the
identifiers available
- preserve thread continuity across `user_changed_number` and
`user_changed_user_id` system messages
- update WhatsApp types and documentation for the new identity fields
and authentication-template limitation
- closes #794

---------

Signed-off-by: dancer <josh@afterima.ge>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Pablo Botta <886512+p4bl1t0@users.noreply.github.com>
2026-08-19 01:11:16 +10:00
Bryan Hunter 929878b56f fix(chat): allow link button IDs in JSX (#838)
- Accept the documented optional `id` prop in the `LinkButton` JSX
runtime guard.
- Preserve the action ID in the resulting `LinkButtonElement`.


## Discovery

We found this when Omniagent’s Slack sign-in card used the documented
`<LinkButton id="…" url="…">` API and Chat SDK’s preview renderer threw
`LinkButton requires a 'url' prop` despite receiving one.

## Root cause

The `!('id' in props)` check was introduced when `LinkButton` did not
support IDs, as a structural distinction from `Button`. Stable
link-button IDs were later added in #598 across `LinkButtonProps`,
`LinkButtonOptions`, `LinkButtonElement`, JSX resolution, adapters, and
the public documentation, but the old JSX guard was not updated.

Component identity is already established by `type === LinkButton`
before this guard runs. Requiring a string `url` is therefore sufficient
and restores the intended API without changing button dispatch or
adapter behavior.

Signed-off-by: bryan-hunter <bryan.hunter@vercel.com>
2026-08-19 00:13:35 +10:00
github-actions[bot] aea4d753de chore(release): version packages (#829)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @chat-adapter/discord@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
  - @chat-adapter/shared@4.38.1
## @chat-adapter/gchat@4.38.1

### Patch Changes

- 3268703: download attachments through the Google Chat media API to
prevent credential exposure
- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
  - @chat-adapter/shared@4.38.1
## @chat-adapter/github@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
  - @chat-adapter/shared@4.38.1
## @chat-adapter/instagram@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
  - @chat-adapter/shared@4.38.1
## @chat-adapter/linear@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
  - @chat-adapter/shared@4.38.1
## @chat-adapter/messenger@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
  - @chat-adapter/shared@4.38.1
## @chat-adapter/notion@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
  - @chat-adapter/shared@4.38.1
## @chat-adapter/shared@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
## @chat-adapter/slack@4.38.1

### Patch Changes

- 764e475: preserve pasted Slack tables in parsed message content:
`table` and `data_table` blocks become structured table nodes in
`message.formatted` and tab-separated text in `message.text`. Table
cells render mentions, channels, and links through the same mrkdwn
converter as body text, unfurl and app attachments are excluded, date
cells without a fallback are formatted from their timestamp, and tables
pasted above the message text stay above it. Headerless pasted tables
get an empty header row so GFM re-serialization doesn't promote the
first data row to a header. `SlackEvent.blocks` is now typed with the
exported `SlackMessageBlock` interface.
- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
  - @chat-adapter/shared@4.38.1
## @chat-adapter/teams@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
  - @chat-adapter/shared@4.38.1
## @chat-adapter/telegram@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
  - @chat-adapter/shared@4.38.1
## @chat-adapter/twilio@4.38.1

### Patch Changes

- d8103a1: prevent credentials from being sent to untrusted media
origins
- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
  - @chat-adapter/shared@4.38.1
## @chat-adapter/web@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
  - @chat-adapter/shared@4.38.1
## @chat-adapter/whatsapp@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
  - @chat-adapter/shared@4.38.1
## @chat-adapter/x@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
  - @chat-adapter/shared@4.38.1
## chat@4.38.1

### Patch Changes

- 6cb933e: isolate queued and debounced messages by thread when using
channel-scoped locks
- 764e475: `toPlainText` keeps empty table cells so tab-separated
columns stay aligned, and drops table rows with no content (such as
placeholder header rows)
## @chat-adapter/state-ioredis@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
## @chat-adapter/state-memory@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
## @chat-adapter/state-pg@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
## @chat-adapter/state-redis@4.38.1

### Patch Changes

- Updated dependencies [6cb933e]
- Updated dependencies [764e475]
  - chat@4.38.1
## @chat-adapter/tests@4.38.1

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-17 22:04:34 +01:00
josh 6cb933ebe2 fix(chat): isolate channel-scoped queue dispatch by thread (#832)
## summary

- dispatch queued, debounced, and burst messages using the dequeued
message's thread id instead of the lock holder's thread id
- restrict skipped message context and queue logs to the dispatched
message's thread
- prevent subscriptions, mentions, state, and replies from crossing
thread boundaries
- add regressions for queue and debounce with channel-scoped locks

---------

Signed-off-by: dancer <josh@afterima.ge>
2026-08-17 21:53:07 +01:00
josh d8103a103c fix(twilio): restrict authenticated media downloads (#831)
## summary

- validate media URLs against the configured Twilio API origin before
resolving credentials
- reject protocol, hostname, and port mismatches without making a
network request
- preserve support for configured regional Twilio API origins
- document that `apiUrl` defines the trusted origin for media downloads

## test plan

- added API-level coverage for trusted regional origins and untrusted
URL variants
- added adapter-level coverage for rehydrated attachments from untrusted
origins
- ran the Twilio build, tests, typecheck, integration tests, and
formatting checks

Signed-off-by: dancer <josh@afterima.ge>
2026-08-17 21:01:49 +01:00
josh 3268703894 fix(gchat): use media api for attachment downloads (#830)
## summary

- use Google Chat `media.download` with `attachmentDataRef.resourceName`
as the only attachment byte download path
- remove the unsupported `downloadUri` fallback and URL-only `fetchData`
rehydration
- preserve `downloadUri` as attachment metadata for human access
- add regression coverage for media download failures and URL-only
attachments
2026-08-17 21:01:31 +01:00
josh 764e4759bd fix(slack): preserve pasted tables in message content (#817)
- parse Slack table blocks from both top-level blocks and attachment
blocks
- preserve pasted spreadsheet data in formatted mdast and plain message
text
- support rich text, raw text, and numeric table cells
- ignore malformed table blocks without dropping valid content
- fixes #803

---------

Signed-off-by: dancer <josh@afterima.ge>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-16 21:31:26 +10:00
github-actions[bot] caab5c3843 chore(release): version packages (#805)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @chat-adapter/discord@4.38.0

### Minor Changes

- a0cba02: Add Vercel Connect credential resolvers and custom webhook
verification to the Discord adapter, with `create-chat-sdk --connect`
scaffolding for Discord bots.

### Patch Changes

- a94995e: Restore attachment downloads across Discord inbound message
paths.
- 4bdf721: Preserve content and attachments from forwarded Discord
message snapshots.
- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
  - @chat-adapter/shared@4.38.0
## @chat-adapter/messenger@4.38.0

### Minor Changes

- 18d4a23: Add a shared thread API for marking messages as read across
WhatsApp, Messenger, and XChat.

Note for anyone calling `XchatAdapter.markAsRead()` directly: it now
rejects when a receipt fails instead of logging a warning and resolving.
Automatic read receipts are unaffected, since the adapter still catches
and logs those internally. If you call the method yourself without
awaiting it, add a `.catch()` so a failed receipt does not surface as an
unhandled rejection.

### Patch Changes

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
  - @chat-adapter/shared@4.38.0
## @chat-adapter/notion@4.38.0

### Minor Changes

- 06b04ac: Add outbound-only Vercel Connect authentication for Notion
while retaining native webhook verification and scaffolding.

### Patch Changes

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
  - @chat-adapter/shared@4.38.0
## @chat-adapter/telegram@4.38.0

### Minor Changes

- 0f24cc3: Preserve normalized replied-to message context and populate
it from Telegram replies.
- 7a1150c: Add outbound-only Vercel Connect authentication for Telegram
while retaining native webhook verification or polling.
- 3bbf3ff: Use post-and-edit streaming by default and make native
Telegram drafts opt-in. Streams now render the same way in every chat
type; set the new `nativeStreaming: true` config option to restore draft
previews in private chats.

The adapter now owns the post-and-edit loop so edits stay under
Telegram's per-chat rate limit. Edits are throttled to a 1100ms floor,
configurable with the new `streamingEditIntervalMs` option, and a
rate-limited final edit is retried once instead of failing the post.

### Patch Changes

- 1d2b78d: Deduplicate repeated Telegram webhook updates by their update
ID using the configured state adapter.
- 745fdf5: respect Telegram streaming rate limits and target XChat read
receipts exactly
- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
  - @chat-adapter/shared@4.38.0
## @chat-adapter/whatsapp@4.38.0

### Minor Changes

- 83ede7e: add native message replies with WhatsApp contextual reply
support
- 18d4a23: Add a shared thread API for marking messages as read across
WhatsApp, Messenger, and XChat.

Note for anyone calling `XchatAdapter.markAsRead()` directly: it now
rejects when a receipt fails instead of logging a warning and resolving.
Automatic read receipts are unaffected, since the adapter still catches
and logs those internally. If you call the method yourself without
awaiting it, add a `.catch()` so a failed receipt does not surface as an
unhandled rejection.

### Patch Changes

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
  - @chat-adapter/shared@4.38.0
## @chat-adapter/x@4.38.0

### Minor Changes

- 18d4a23: Add a shared thread API for marking messages as read across
WhatsApp, Messenger, and XChat.

Note for anyone calling `XchatAdapter.markAsRead()` directly: it now
rejects when a receipt fails instead of logging a warning and resolving.
Automatic read receipts are unaffected, since the adapter still catches
and logs those internally. If you call the method yourself without
awaiting it, add a `.catch()` so a failed receipt does not surface as an
unhandled rejection.

### Patch Changes

- 745fdf5: respect Telegram streaming rate limits and target XChat read
receipts exactly
- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
  - @chat-adapter/shared@4.38.0
## chat@4.38.0

### Minor Changes

- 0f24cc3: Preserve normalized replied-to message context and populate
it from Telegram replies.
- bdeb2bf: Add a workflow-safe `chat/serialization` entrypoint and
isolate automatic Chat class serializers from Node-only runtime
dependencies.
- a0cba02: Add Vercel Connect credential resolvers and custom webhook
verification to the Discord adapter, with `create-chat-sdk --connect`
scaffolding for Discord bots.
- 83ede7e: add native message replies with WhatsApp contextual reply
support
- 18d4a23: Add a shared thread API for marking messages as read across
WhatsApp, Messenger, and XChat.

Note for anyone calling `XchatAdapter.markAsRead()` directly: it now
rejects when a receipt fails instead of logging a warning and resolving.
Automatic read receipts are unaffected, since the adapter still catches
and logs those internally. If you call the method yourself without
awaiting it, add a `.catch()` so a failed receipt does not surface as an
unhandled rejection.
## create-chat-sdk@0.4.0

### Minor Changes

- a0cba02: Add Vercel Connect credential resolvers and custom webhook
verification to the Discord adapter, with `create-chat-sdk --connect`
scaffolding for Discord bots.
- 06b04ac: Add outbound-only Vercel Connect authentication for Notion
while retaining native webhook verification and scaffolding.
- 7a1150c: Add outbound-only Vercel Connect authentication for Telegram
while retaining native webhook verification or polling.
## @chat-adapter/gchat@4.38.0

### Patch Changes

- 2f40a32: Fix attachment downloads failing with a 400 when
`attachmentDataRef` is present

`fetchAttachmentData` called the Chat media endpoint without
`alt=media`, so it
returned resource metadata rather than file bytes and rejected the
arraybuffer
request with a bare 400. Every download by `resourceName` failed, which
is the
  path taken for any file uploaded directly to Chat.

The download path is also hardened: when `media.download` fails and the
attachment carries a `downloadUri`, the adapter now falls back to
fetching that
URL instead of rejecting, and failures with no fallback are routed
through the
shared error handling so a 429 surfaces as `AdapterRateLimitError` like
every
  other Chat API call.

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
  - @chat-adapter/shared@4.38.0
## @chat-adapter/github@4.38.0

### Patch Changes

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
  - @chat-adapter/shared@4.38.0
## @chat-adapter/instagram@4.38.0

### Patch Changes

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
  - @chat-adapter/shared@4.38.0
## @chat-adapter/linear@4.38.0

### Patch Changes

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
  - @chat-adapter/shared@4.38.0
## @chat-adapter/shared@4.38.0

### Patch Changes

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
## @chat-adapter/slack@4.38.0

### Patch Changes

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
  - @chat-adapter/shared@4.38.0
## @chat-adapter/teams@4.38.0

### Patch Changes

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
  - @chat-adapter/shared@4.38.0
## @chat-adapter/twilio@4.38.0

### Patch Changes

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
  - @chat-adapter/shared@4.38.0
## @chat-adapter/web@4.38.0

### Patch Changes

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
  - @chat-adapter/shared@4.38.0
## @chat-adapter/state-ioredis@4.38.0

### Patch Changes

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
## @chat-adapter/state-memory@4.38.0

### Patch Changes

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
## @chat-adapter/state-pg@4.38.0

### Patch Changes

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
## @chat-adapter/state-redis@4.38.0

### Patch Changes

- Updated dependencies [0f24cc3]
- Updated dependencies [bdeb2bf]
- Updated dependencies [a0cba02]
- Updated dependencies [83ede7e]
- Updated dependencies [18d4a23]
  - chat@4.38.0
## @chat-adapter/tests@4.38.0

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-14 20:28:19 +01:00
Max 4bdf7213b6 fix(discord): preserve forwarded message snapshots (#825)
Discord forwards place the original content and attachments under
`message_snapshots`, while the outer message fields are empty. The
Discord adapter currently reads only the outer fields in both direct
Gateway and forwarded-webhook modes, so forwarded voice notes and files
arrive as blank messages.

This flattens the snapshot content and attachments into the normalized
Chat SDK message while preserving any outer content and attachments.

Verified with focused regressions for both Gateway modes, the full
Discord adapter test suite (285 tests), typecheck, build, and repository
formatting checks.

---------

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
Signed-off-by: dancer <josh@afterima.ge>
Co-authored-by: dancer <josh@afterima.ge>
2026-08-14 20:01:22 +01:00
josh 745fdf5a97 fix(adapters): harden Telegram streaming and XChat read receipts (#826)
## summary

- pace Telegram post-and-edit streams for private and non-private chat
limits, including the final edit
- respect Telegram `retry_after` cooldowns and reject when the complete
response cannot be delivered
- prevent explicit XChat read receipts from advancing past an unresolved
message
- preserve latest-event fallback for delivered XChat messages without a
sequence id
- update adapter documentation and regression coverage

---------

Signed-off-by: dancer <josh@afterima.ge>
2026-08-14 19:34:05 +01:00
josh 3bbf3ff542 fix(telegram): make native draft streaming opt-in (#822)
- use post-and-edit streaming by default to avoid leaked draft previews
in Telegram clients
- add `nativeStreaming: true` for explicitly enabling native draft
previews in private chats
- preserve existing native streaming behavior when enabled
- document the client compatibility tradeoff
- closes #782

before: private chat streams used native Telegram drafts by default,
which could remain visible over the final message on Telegram macOS

after: streams use post-and-edit by default across Telegram clients,
while native drafts remain available as an opt-in

---------

Signed-off-by: dancer <josh@afterima.ge>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-14 15:21:48 +10:00
josh 83ede7eab2 feat(chat): add message reply support (#819)
- add `thread.reply()` for sending messages with native references to
existing messages
- accept either a message object from the same thread or a message id as
the reply target
- support text, markdown, AST, cards, files, and buffered streams
- add WhatsApp contextual replies using the Cloud API
`context.message_id` field
- apply reply context only to the first outgoing message when content is
split across multiple sends
- preserve the target message through sent message edits and thread
history
- throw `NotImplementedError` for adapters without native reply support
- document the API and add message replies to the adapter feature matrix

fixes #786

---------

Signed-off-by: dancer <josh@afterima.ge>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Aradhya C P <135510032+aradhyacp@users.noreply.github.com>
2026-08-14 14:32:27 +10:00
josh 18d4a230d7 feat(chat): add mark as read support (#820)
- add `thread.markAsRead()` for the current message, an explicit
`Message`, or a message ID
- expose read receipts as an optional adapter capability with explicit
unsupported and thread mismatch errors
- support WhatsApp read acknowledgements, Messenger `mark_seen`, and
XChat read watermarks
- preserve automatic XChat receipts while allowing manual timing and
surfacing explicit failures
- document provider-specific behavior and capability support
- closes #785

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Aradhya C P <135510032+aradhyacp@users.noreply.github.com>
2026-08-14 14:09:23 +10:00
Ben Sabic 7a1150ce23 Add Vercel Connect support to Telegram (#813)
Adds function-backed Telegram bot-token resolution so the adapter can
use short-lived Vercel Connect credentials for every Bot API and
file-download request. Static tokens retain their existing synchronous
behavior, while native Telegram webhook verification or polling remains
unchanged.

```ts
import { createTelegramAdapter } from "@chat-adapter/telegram";
import { connectTelegramAdapter } from "@vercel/connect/chat";

createTelegramAdapter({
  ...connectTelegramAdapter("telegram/acme-telegram"),
  secretToken: process.env.TELEGRAM_WEBHOOK_SECRET_TOKEN,
});
```

`create-chat-sdk` now recognizes Telegram as Connect-capable, preserves
`TELEGRAM_WEBHOOK_SECRET_TOKEN`, and emits native-webhook guidance:

```bash
npm create chat-sdk@latest -- my-bot --adapter telegram memory --connect -y
```

This PR is stacked on the Notion Connect work in #812. Validated with
the Telegram adapter suite (251 tests), create-chat-sdk suite (211
tests), package type checks/builds, and repository lint/format checks.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-12 11:49:29 +10:00
Ben Sabic 06b04ac4d9 Add Vercel Connect support to Notion (#812)
Adds function-backed Notion access-token resolution so the adapter can
use short-lived Vercel Connect credentials for every API request, retry,
and multipart upload. Direct Notion webhooks continue to use
`NOTION_VERIFICATION_TOKEN` and native HMAC verification because Connect
does not forward Notion triggers.

```ts
import { createNotionAdapter } from "@chat-adapter/notion";
import { connectNotionAdapter } from "@vercel/connect/chat";

createNotionAdapter({
  ...connectNotionAdapter("notion/acme-notion"),
  verificationToken: process.env.NOTION_VERIFICATION_TOKEN,
});
```

`create-chat-sdk` now recognizes Notion as Connect-capable, preserves
the native webhook verification token, and emits direct-webhook
guidance:

```bash
npm create chat-sdk@latest -- my-bot --adapter notion memory --connect -y
```

Validated with the Notion adapter suite (71 tests), create-chat-sdk
suite (209 tests), package type checks/builds, and repository
lint/format checks.

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-12 11:00:21 +10:00
Max 0f24cc3062 feat(chat): preserve replied-to message context (#802)
## Summary

- add optional, normalized `Message.replyTo` context that survives JSON
and workflow serialization, queue rehydration, thread history, and
`SentMessage` reconstruction
- populate it from Telegram's `reply_to_message`, including combined
media groups, so handlers don't need raw Telegram payloads
- keep the core contract adapter-neutral while Telegram owns only its
platform mapping, allowing other adapters to populate it when they
receive full replied-to messages


Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-08-11 17:56:40 +01:00
Max a94995e5b4 fix(discord): restore forwarded attachment downloads (#800)
## Summary

Discord inbound messages created attachment objects without `fetchData`,
so consumers could not download audio or other attachments. The omission
affected forwarded Gateway webhooks, REST and history parsing, and
direct Discord.js Gateway messages.

Route attachments from all three inbound paths through the adapter's
existing `rehydrateAttachment()` implementation. Existing parser and
Gateway tests now assert that the resulting attachments are
downloadable.

---------

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-08-11 17:47:56 +01:00
Max 1d2b78d933 Deduplicate repeated Telegram webhook updates (#799)
## Summary

Telegram retries webhook deliveries after non-2xx responses, and its
`update_id` field is explicitly intended for ignoring repeated updates.
The Telegram adapter previously routed every webhook delivery
independently.

This change atomically claims each integer `update_id` through the
configured `StateAdapter` before routing the update. Repeated deliveries
return 200 without reaching bot handlers, while state failures return
503 without dispatching so Telegram can retry. Updates without an
integer `update_id` keep their existing behavior, and polling remains
unchanged.

Claims expire after 24 hours because Telegram retains incoming updates
for no longer than 24 hours. This is a bounded retention choice, not a
documented retry timeout. Cross-instance deduplication requires shared
durable state; in-memory state only protects one process. The change
provides webhook-delivery idempotency, not end-to-end exactly-once
handler completion.

Telegram contract: [Update](https://core.telegram.org/bots/api#update)
and [setWebhook](https://core.telegram.org/bots/api#setwebhook).

## Test plan

- `pnpm --filter @chat-adapter/telegram test`
- `pnpm --filter @chat-adapter/telegram typecheck`
- `pnpm check`
- `pnpm konsistent`
- `TURBO_CONCURRENCY=1 pnpm validate`

Regression coverage verifies sequential and concurrent repeated
deliveries, distinct update IDs, missing update IDs, duplicate 200
responses, and state-failure retry behavior. GitHub CI also passes on
Node 22 and Node 24.

## Checklist

- [x] All commits are signed and verified
- [x] All commits are signed off for the DCO (`git commit -s`)
- [x] `pnpm validate` passes
- [x] Changeset added (or N/A — see
[CONTRIBUTING.md](./CONTRIBUTING.md))
- [x] Documentation updated (or N/A)

---------

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
Signed-off-by: dancer <josh@afterima.ge>
Co-authored-by: dancer <josh@afterima.ge>
2026-08-11 17:37:50 +01:00
Ben Sabic 927d0dbd7d docs: add cross-link card sections and page-level SEO metadata (#804)
Many docs pages are orphaned: nothing links to them apart from the
sidebar, so readers and crawlers rarely find them. This PR gives every
docs page a Read more section with four cards at the bottom of the
article, above the prev/next footer.

Cards are picked deterministically in lib/read-more.ts: the page's
related frontmatter first, then prerequisites, then siblings from the
same sidebar section, then the rest of the page tree, so every page
always fills all four slots. Card titles and descriptions come from the
target page's own frontmatter, nothing is duplicated. The section is
injected through the MDX wrapper slot in the docs route, so it applies
to all pages without touching content.

To make the links topical rather than positional, 26 pages get related
frontmatter additions. The 20 pages that no other page referenced (all
ten api/ pages among them) now each have at least one inbound link,
generally pairing guides with their API reference and back. The bundled
copy of create-chat-sdk.mdx is synced to keep the byte-match test green.

Official adapter pages get the same treatment with a More adapters
section: same-type adapters first (platform or state, using the catalog
order), topped up from the other official group. Vendor-official and
community adapters are never shown, and their pages don't render the
section. It reuses AdapterCard, so logos and package names match the
listing page.

Two small SEO fixes ride along. JSON-LD was allowlisted to three docs
pages; the allowlist is gone, so all 45 now emit HowTo or TechArticle
plus a BreadcrumbList. Docs and adapter detail pages also emit canonical
URLs now, resolved against the existing metadataBase.

Verified against the production build: all 45 docs pages and all 19
official adapter pages render exactly four cards, no page is left
unreferenced, canonicals and JSON-LD are present everywhere, and pnpm
validate passes. Docs-only, so no changeset.

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-11 08:12:05 +10:00
Ben Sabic a0cba0288a Add Vercel Connect support to Discord (#808)
Adds function-backed Discord bot token and application ID resolvers,
plus custom webhook verification for Vercel Connect trigger-forwarded
interactions. Native Discord Ed25519 verification remains the default
when no custom verifier is configured.

```ts
import { createDiscordAdapter } from "@chat-adapter/discord";
import { connectDiscordAdapter } from "@vercel/connect/chat";

createDiscordAdapter({
  ...connectDiscordAdapter("discord/acme-discord"),
});
```

`create-chat-sdk` now recognizes Discord as Connect-capable, generates
`DISCORD_CONNECTOR` instead of native credential variables, and
preserves `CRON_SECRET` for Gateway forwarding:

```bash
npm create chat-sdk@latest -- my-bot --adapter discord memory --connect -y
```

Validated with the Discord adapter suite (284 tests), create-chat-sdk
suite (206 tests), package type checks/builds, and repository
lint/format checks.

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-11 08:11:32 +10:00
Bryan Hunter bdeb2bf1b1 fix(workflow): isolate chat serializers from node runtime (#806)
## Failure

Workflow SDK `5.0.0-beta.40` produces an invalid workflow bundle when a
Chat SDK serializable class such as `Message`, `ThreadImpl`, or
`ChannelImpl` crosses a workflow step boundary.

The Workflow compiler imports the emitted module containing each class
to register its `@workflow/serde` methods. In Chat SDK `4.37.0`, tsup
emits those classes in `dist/index.js`. The root entry also imports the
conversation-scoping implementation added in #751, which uses
`AsyncLocalStorage` from `node:async_hooks`. Serializer registration
therefore pulls Node-only code into the sandboxed workflow bundle before
any workflow or step executes.

Build warning:

```text
Serde warning for classes "ChannelImpl", "Message", "ThreadImpl":
Workflow bundle contains Node.js built-in imports: async_hooks.
These will fail at runtime in the workflow sandbox.
```

Deployed workflows then fail during module initialization:

```text
var import_async_hooks = require("async_hooks");
                         ^

ReferenceError: require is not defined
```

## Minimal reproduction

```json
{
  "dependencies": {
    "chat": "4.37.0",
    "workflow": "5.0.0-beta.40"
  }
}
```

```ts
import { Message } from "chat";

async function createMessageStep(value: string): Promise<Message> {
  "use step";

  return new Message({
    id: "message",
    threadId: "slack:C123:123.456",
    text: value,
    formatted: {
      type: "root",
      children: [
        {
          type: "paragraph",
          children: [{ type: "text", value }],
        },
      ],
    },
    raw: {},
    author: {
      userId: "U123",
      userName: "user",
      fullName: "User",
      isBot: false,
      isMe: false,
    },
    metadata: { dateSent: new Date(), edited: false },
    attachments: [],
  });
}

export async function testWorkflow(value: string): Promise<string> {
  "use workflow";

  const message = await createMessageStep(value);
  return message.text;
}
```

Running `workflow build` on `4.37.0` emits the warning; deploying the
output produces the runtime failure above.

## Fix

- Add a dedicated `chat/serialization` package entry for `Message`,
`ThreadImpl`, `ChannelImpl`, `reviver`, and their serialized DTO types.
- Make serializer code a second tsup entry and explicitly enable
splitting. The serializer-bearing classes are now emitted into a shared
chunk with no dependency on `Chat` or its Node-only conversation
context.
- Preserve the existing root exports and automatic `@workflow/serde`
behavior. Existing `import { Message } from "chat"` workflow code
remains valid.
- Add a post-build module-graph assertion that fails if any emitted
serializer registration can transitively import a Node.js builtin.
- Test against Workflow SDK `5.0.0-beta.40`, the compiler version that
exposed the invalid bundle.
- Add a minor changeset for the fixed-version Chat SDK packages,
producing the `4.38.0` release line.

After the change, the emitted serializer classes live in a sandbox-safe
shared chunk while `AsyncLocalStorage` remains in a separate Node
runtime chunk. The exact reproduction compiles successfully with `5
steps, 1 workflow` and no Serde warning.

## Control cases

The failure requires a serializable Chat class to cross a durable
boundary. These cases were already safe and remain unchanged:

- `AsyncLocalStorage` used entirely inside a `"use step"` function.
- A Chat `Message` created and consumed within one step while returning
plain data.
- Request handlers that convert Chat objects to plain workflow DTOs
before starting a workflow.
- `@vercel/sandbox` used entirely inside a step.

## Validation

- Committed beta.40 reproduction fixture: type-correct and compiled
during every Chat package build with no Node builtin / Serde warning.
- Emitted serializer module graph: no transitive Node.js builtins.
- Chat package: 1,113 tests pass.
- Chat package typecheck passes.
- Repository formatting and lint checks pass.
- Package build passes.

Full repository validation reaches the pre-existing `knip` baseline and
reports unrelated unused dependencies and unlisted binaries in examples
and adapter packages.

---------

Signed-off-by: bryan-hunter <bryan.hunter@vercel.com>
2026-08-10 09:03:49 -05:00
Cody Buell 2f40a322e2 fix(gchat): send alt=media when downloading attachments by resourceName (#801)
## Problem

`GoogleChatAdapter.fetchAttachmentData` calls the Chat media endpoint
without
`alt=media`:

```ts
const res = await this.chatApi.media.download(
  { resourceName },
  { responseType: "arraybuffer" }
);
```

Without that parameter the endpoint returns resource metadata rather
than the
file bytes, and the `responseType: "arraybuffer"` request fails with a
bare
`400`.

This is hit by every attachment carrying an `attachmentDataRef` — the
path
taken by any file uploaded directly to Chat, as opposed to linked from
Drive —
so image and file downloads fail uniformly. Consumers see only a 400 out
of
`fetchData()`, with nothing pointing at a missing parameter. Downstream
it
tends to present as "the agent was sent a file and can't find it", since
the
attachment metadata still arrives and only the bytes are missing.

## Verification

Confirmed against the live API with a real attachment, using the same
auth and
the same `resourceName`, varying only the parameter:

| Request | Result |
|---|---|
| `GET /v1/media/{resourceName}` | `400` |
| `GET /v1/media/{resourceName}?alt=media` | `200`, 509,644 bytes |

## Fix

Pass `alt: "media"`. The generated `@googleapis/chat` client doesn't
declare
`alt` on `media.download`, so it goes through as an extra param and
lands in
the query string.

## Tests

The two existing tests asserted the exact call shape against a mocked
`media.download`, so they passed while the real call could not succeed —
the
mock encoded the broken shape. Both assertions now expect `alt:
"media"`.

Reverting the source change alone turns them red, so they guard the fix:

```
✗ should use media.download API when attachmentDataRef is present
✗ should provide fetchData when only attachmentDataRef is present (no downloadUri)
```

`packages/adapter-gchat` is green with the change: **264 passed**.

---------

Signed-off-by: codybuell <cody@codybuell.com>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-10 10:41:55 +10:00
github-actions[bot] 3468cdfe0b chore(release): version packages (#767)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @chat-adapter/gchat@4.37.0

### Minor Changes

- c3b5a08: Bind Pub/Sub push verification to a specific identity with
the new pubsubServiceAccountEmail option, alongside the existing
audience check. Pushes are rejected unless the token email matches it.
Direct webhooks are unaffected.
- 7a19223: Bind Workspace Add-on webhook verification to a specific
identity with the new `workspaceAddOnServiceAccountEmail` option,
replacing a pattern match on the add-on service account email. Workspace
Add-on Chat apps must set it; standalone Chat apps are unaffected.

### Patch Changes

- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
  - @chat-adapter/shared@4.37.0
## @chat-adapter/instagram@4.37.0

### Minor Changes

- 2a2b2c5: Add a native Instagram Direct Messages adapter with signed
webhooks, media, quick replies, story context, reactions, and typed Meta
API errors.

### Patch Changes

- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
  - @chat-adapter/shared@4.37.0
## @chat-adapter/notion@4.37.0

### Minor Changes

- 0ec6a73: Add `@chat-adapter/notion` for Notion page and block comment
discussions: webhook HMAC verification, Post+Edit streaming,
conversation history, `message.subject` page metadata, plain-text
`@userName`/`@botUserId` mention detection, and File Uploads (up to 3
native attachments). Registers the adapter in the `chat/adapters`
catalog and `create-chat-sdk` CLI scaffold, and adds Notion emoji
platform support.

### Patch Changes

- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
  - @chat-adapter/shared@4.37.0
## @chat-adapter/slack@4.37.0

### Minor Changes

- 4ac0455: Add message update and delete lifecycle callbacks, with Slack
message_changed and message_deleted dispatch support.

### Patch Changes

- 6f0d2f0: Resolve outgoing @name mentions on the Slack native streaming
path so streamed responses mention users consistently with the
post-and-edit fallback. Committed renderer text is resolved
incrementally, keeping fenced code literal and preserving the existing
ambiguity semantics.
- 4cc3445: Bound the length of bracketed URLs parsed from message text
in the link-unfurl fallback, avoiding a quadratic scan on adversarial
input. Valid links are unaffected.
- c311827: Preserve the Slack channel ID when converting labeled channel
tokens (`<#C123|general>` now becomes `#general (C123)`) so agents can
pass the ID to channel tools, and normalize the commonly hallucinated
`<label|url>` link order before Markdown conversion
- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
  - @chat-adapter/shared@4.37.0
## @chat-adapter/whatsapp@4.37.0

### Minor Changes

- 6abf480: Add native WhatsApp LinkButton support

- A card whose only interactive element is a single `LinkButton` with a
non-empty label and an `http://` or `https://` URL is now sent as a
native `cta_url` interactive message, as long as the card has no header
image or image, table, chart, or inline link children and the post
carries no files or attachments.
- Link button URLs are now appended as `Label: url` lines to interactive
button message bodies and to media captions, instead of being dropped.
- Everything else is unchanged: non-matching cards keep the formatted
text fallback, and card + media posts keep the single captioned media
send.

- 16879fd: Fix the `WhatsAppInboundMessage.context` type to model all
documented webhook variants. The type previously declared `context?: {
from: string; id: string }`, but Meta's Cloud API sends mutually
exclusive context shapes: quoted replies carry `from`/`id`, forwarded
messages carry only `forwarded` or `frequently_forwarded` (no `id`), and
catalog product inquiries add `referred_product`. Code narrowed by the
old type could dereference `context.id` and crash at runtime on
forwarded messages. All context fields are now optional and the
forwarded/product-inquiry fields are included. Consumers that
dereference `context.from` or `context.id` without a guard will now see
a type error, surfacing what was already a latent crash on forwarded
messages.

### Patch Changes

- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
  - @chat-adapter/shared@4.37.0
## chat@4.37.0

### Minor Changes

- 2a2b2c5: Add a native Instagram Direct Messages adapter with signed
webhooks, media, quick replies, story context, reactions, and typed Meta
API errors.
- 4ac0455: Add message update and delete lifecycle callbacks, with Slack
message_changed and message_deleted dispatch support.
- 0ec6a73: Add `@chat-adapter/notion` for Notion page and block comment
discussions: webhook HMAC verification, Post+Edit streaming,
conversation history, `message.subject` page metadata, plain-text
`@userName`/`@botUserId` mention detection, and File Uploads (up to 3
native attachments). Registers the adapter in the `chat/adapters`
catalog and `create-chat-sdk` CLI scaffold, and adds Notion emoji
platform support.
- 85e3d22: Close residual gaps in agent read-tool scoping.
`createChatTools`'s read guard now wraps modal, assistant-thread,
assistant-context, app-home, app-context, and member-joined dispatch so
tools built in those handlers inherit the active conversation, and it
logs a warning (instead of failing open silently) when a read runs with
no resolvable scope. Scoping stays channel-level by default, so a thread
scope still permits sibling threads in its channel. Pass the new
`strictScope: true` to confine a thread scope to that thread alone,
rejecting both sibling threads and the parent channel, which matters on
platforms where a channel is the widest read available (a GitHub channel
is an entire repo).

Note that reads inside those newly wrapped handlers were previously
unscoped. An agent built in an `onModalSubmit`, `onAppHomeOpened`, or
`onMemberJoinedChannel` handler that reads another channel will now be
rejected. Pass an explicit `scope`, or `scope: false` for intentionally
workspace-wide reads.
## create-chat-sdk@0.3.0

### Minor Changes

- 2a2b2c5: Add a native Instagram Direct Messages adapter with signed
webhooks, media, quick replies, story context, reactions, and typed Meta
API errors.
- 0ec6a73: Add `@chat-adapter/notion` for Notion page and block comment
discussions: webhook HMAC verification, Post+Edit streaming,
conversation history, `message.subject` page metadata, plain-text
`@userName`/`@botUserId` mention detection, and File Uploads (up to 3
native attachments). Registers the adapter in the `chat/adapters`
catalog and `create-chat-sdk` CLI scaffold, and adds Notion emoji
platform support.
## @chat-adapter/discord@4.37.0

### Patch Changes

- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
  - @chat-adapter/shared@4.37.0
## @chat-adapter/github@4.37.0

### Patch Changes

- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
  - @chat-adapter/shared@4.37.0
## @chat-adapter/linear@4.37.0

### Patch Changes

- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
  - @chat-adapter/shared@4.37.0
## @chat-adapter/messenger@4.37.0

### Patch Changes

- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
  - @chat-adapter/shared@4.37.0
## @chat-adapter/shared@4.37.0

### Patch Changes

- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
## @chat-adapter/teams@4.37.0

### Patch Changes

- 4cc3445: Harden Teams HTML-to-text conversion to strip tags until the
output is stable, so nested or malformed markup can't leave a partial
tag behind. `stripHtmlTags` is now shared across the format and Graph
message converters.
- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
  - @chat-adapter/shared@4.37.0
## @chat-adapter/telegram@4.37.0

### Patch Changes

- 629e655: Combine incoming Telegram media groups into one message with
ordered attachments and the shared caption.
- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
  - @chat-adapter/shared@4.37.0
## @chat-adapter/twilio@4.37.0

### Patch Changes

- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
  - @chat-adapter/shared@4.37.0
## @chat-adapter/web@4.37.0

### Patch Changes

- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
  - @chat-adapter/shared@4.37.0
## @chat-adapter/x@4.37.0

### Patch Changes

- b674923: Restrict the X CRC challenge to the opaque token shape X
sends before signing it. The endpoint previously returned an HMAC over
any `crc_token`, which let a caller have an arbitrary webhook body
signed and replay that as `x-twitter-webhooks-signature` on a forged
POST. A webhook body is JSON and can no longer pass the token check, so
a CRC response can't double as a POST event signature.
- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
  - @chat-adapter/shared@4.37.0
## @chat-adapter/state-ioredis@4.37.0

### Patch Changes

- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
## @chat-adapter/state-memory@4.37.0

### Patch Changes

- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
## @chat-adapter/state-pg@4.37.0

### Patch Changes

- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
## @chat-adapter/state-redis@4.37.0

### Patch Changes

- Updated dependencies [2a2b2c5]
- Updated dependencies [4ac0455]
- Updated dependencies [0ec6a73]
- Updated dependencies [85e3d22]
  - chat@4.37.0
## @chat-adapter/tests@4.37.0

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-07 18:30:01 +01:00
josh c3b5a08e7e fix(gchat): bind Pub/Sub push verification to a configured identity (#797)
## summary

Pub/Sub push verification checked the token's `aud` and nothing else.
[Google's
guidance](https://docs.cloud.google.com/pubsub/docs/authenticate-push-subscriptions)
is explicit that signature and audience verification are not sufficient
on their own, and that the `email` and `email_verified` claims must be
checked alongside them

adds `pubsubServiceAccountEmail` (env
`GOOGLE_CHAT_PUBSUB_SERVICE_ACCOUNT_EMAIL`), the identity in the
subscription's push auth settings. a push is accepted only when
`email_verified` is true and `email` matches exactly. when the option is
unset, pushes are rejected rather than trusted on their audience alone

direct webhooks are untouched, and the project-number path already bound
to an exact issuer

### how it happened

`verifyBearerToken` took the claim validator as an optional parameter,
so a call site could simply omit it, and the Pub/Sub one did while the
direct-webhook one did not. that is now required:

```diff
-    validatePayload?: (payload: {
+    validatePayload: (payload: {
```

both call sites pass one and the type system enforces it, so the
omission cannot recur

## test plan

- a token from a different service account is rejected
- a token is rejected when no identity is configured
- a token is rejected when `email_verified` is not true
- a token with no `email` claim is rejected
- a matching identity with a verified email is accepted
- direct-webhook and project-number verification are unchanged

docs cover the new option in the README and adapter page, including the
push-subscription authentication step that produces the token
2026-08-07 17:54:40 +01:00
Ben Sabic 2a2b2c5500 feat(instagram): add native DM adapter (#770)
Adds a first-party Instagram Direct Messages adapter backed by Meta's
Instagram API with Instagram Login.

- Verifies webhook challenges and HMAC signatures, then normalizes DMs,
story replies, media, quick replies, postbacks, and reactions.
- Sends plain text, cards, quick replies, typing indicators, URL
attachments, and uploaded media through `graph.instagram.com`.
- Maps authentication, rate-limit, and 24-hour messaging-window failures
to typed adapter errors.
- Registers Instagram in the adapter catalog, CLI scaffold, official
docs, replay suite, and Next.js example.

## Usage

```ts
import { createInstagramAdapter } from "@chat-adapter/instagram";
import { Chat } from "chat";

const bot = new Chat({
  userName: "mystore",
  adapters: { instagram: createInstagramAdapter() },
});
```

## Webhook

```ts
export async function POST(request: Request) {
  return bot.webhooks.instagram(request);
}
```

## Verification

- `pnpm --filter @chat-adapter/instagram test`
- `pnpm --filter @chat-adapter/instagram typecheck`
- `pnpm --filter example-nextjs-chat typecheck`
- `pnpm --filter example-nextjs-chat build`
- `pnpm check`
- `pnpm konsistent`

## Live Testing

<table>
  <tr>
<td><img width="1440" height="2109" alt="1000000502"
src="https://github.com/user-attachments/assets/9fdb8c3b-4e41-4c81-9426-08756a5e4201"
/></td>
<td><img width="1440" height="1995" alt="1000000503"
src="https://github.com/user-attachments/assets/8a572493-c57a-4412-9049-5737aaa9dfd0"
/></td>
  </tr>
</table>

Closes #729 / Co-Authored by @ivandujaut

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-07 17:38:15 +01:00
Aradhya C P 6abf4807db feat(whatsapp): Add native LinkButton support for WhatsApp CTA URL messages (#781)
Adds native `LinkButton` support to the WhatsApp adapter by
mapping Chat SDK `LinkButton` actions to WhatsApp Cloud API CTA URL
interactive messages.

Previously, WhatsApp cards containing only `LinkButton` actions were
rendered as plain text with the URL exposed. WhatsApp supports native
CTA URL buttons through `interactive.type: "cta_url"`, so this change
enables the adapter to use that native capability.

Closes #780

## Changes Made

- Added support for converting a single `LinkButton` action into a
WhatsApp CTA URL interactive message.
- Added the `cta_url` interactive message shape to the WhatsApp adapter
types.
- Preserved existing reply button behavior and fallback handling for
unsupported card configurations.
- Added test coverage for:
  - Single `LinkButton` → native CTA URL message conversion.
  - Existing reply button behavior remaining unchanged.
  - Multiple `LinkButton` fallback behavior.

### Test Coverage

Added tests covering the new CTA URL conversion path and verified the
generated WhatsApp payload contains:

- `interactive.type: "cta_url"`
- `action.name: "cta_url"`
- `action.parameters.display_text`
- `action.parameters.url`

## Screenshots/Demos

<img width="864" height="338" alt="image"
src="https://github.com/user-attachments/assets/cc58a76b-5a96-406a-9f79-ca7a2725836b"
/>

## Additional Notes

WhatsApp CTA URL messages only support a single URL button per
interactive message. The implementation intentionally only promotes
cards with exactly one `LinkButton` into a CTA URL message and keeps
existing fallback behavior for unsupported combinations.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-07 15:17:47 +10:00
Ben Sabic 0ec6a7361b feat(notion): add Notion comments adapter (#689)
Adds `@chat-adapter/notion`, an official adapter that lets a Chat SDK
bot take part in **Notion comment discussions** (page-level and
block/discussion threads) with the same handler code used for Slack,
Linear, GitHub, etc. Inbound events arrive via Notion webhooks
(`comment.created`) with HMAC signature verification; outbound actions
use the Comments REST API. Because Notion lets a connection edit its own
comments, the adapter supports **Post+Edit streaming**.

### Highlights

- **Webhooks** — `comment.created` verified with `X-Notion-Signature`
HMAC over the raw body (timing-safe), plus the one-time
`verification_token` handshake. Returns a fast 200 with idempotent,
state-backed dedupe.
- **Post+Edit streaming** — posts the first chunk, then `PATCH`es the
comment as tokens arrive, throttled to Notion's ~3 req/s limit (global
token bucket, `Retry-After` aware). Long bodies are split into
sequential comments to stay under the 2000-char rich-text cap.
- **Mentions** — three modes: `mention` (default; plain-text `@userName`
/ `@botUserId`), `all-comments`, and `keyword`.
- **`message.subject`** — resolves the parent page via the Pages API
(title, url, archived status, author).
- **File uploads** — up to 3 native attachments via the File Uploads API
(binary `single_part`; public URLs via `external_url` with bounded
polling); overflow and failures fall back to markdown links.
- **History** — `fetchMessages` over list-comments (open comments only),
direction-aware.
- Cards render as markdown fallback; reactions / typing / DMs are typed
no-ops or errors. Registered in the `chat/adapters` catalog and the
`create-chat-sdk` scaffold; pinned to `Notion-Version: 2026-03-11`.

### Usage

```ts
// lib/bot.ts
import { Chat } from "chat";
import { createNotionAdapter } from "@chat-adapter/notion";
import { createRedisState } from "@chat-adapter/state-redis";

export const bot = new Chat({
  userName: "notion-bot",
  adapters: { notion: createNotionAdapter() }, // reads NOTION_TOKEN + NOTION_VERIFICATION_TOKEN
  state: createRedisState(),
});

bot.onNewMention(async (thread, message) => {
  const subject = await message.subject; // parent page metadata (title, url, …)
  await thread.post(`Thanks for the mention on **${subject?.title ?? "this page"}**!`);
});
```

```ts
// app/api/webhooks/notion/route.ts
import { bot } from "@/lib/bot";

export const POST = (request: Request): Promise<Response> => bot.webhooks.notion(request);
```

### Configuration

Auto-detects `NOTION_TOKEN` and `NOTION_VERIFICATION_TOKEN`, plus
optional `NOTION_BOT_USERNAME`, `NOTION_MENTION_MODE`,
`NOTION_KEYWORDS`, and `NOTION_VERSION`; everything is overridable via
`createNotionAdapter({ … })`. The docs page covers the full connection +
webhook setup (capabilities, content access, and the webhook-URL-lock
warning).

Changeset bumps `@chat-adapter/notion`, `chat`, and `create-chat-sdk`
(minor). Layered as four commits: `feat` (adapter +
catalog/scaffold/emoji), `docs`, `test`, `chore(example)`.

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: dancer <josh@afterima.ge>
2026-08-05 14:15:44 +01:00
josh 4ac0455134 feat(chat): add message update and delete lifecycle callbacks (#788)
## summary

adds `onMessageUpdated` and `onMessageDeleted`, so a bot can react when
a message is edited or removed. Slack dispatches both today; other
adapters can opt in later

supersedes #549, which was verified there against real Slack webhooks.
reopened from a branch in this repo with the original commits preserved
and signed

```typescript
bot.onMessageUpdated(async (thread, message, previousMessage) => {
  await mirror.update(message.id, message.text);
});

bot.onMessageDeleted(async (event) => {
  await mirror.remove(event.messageId);
});
```

both are lifecycle events: they never route through `onNewMessage`,
`onNewMention`, or `onSubscribedMessage`, and the concurrency strategies
do not apply

### notes

- **the bot's own edits are filtered.** slack sends a `message_changed`
for every `chat.update`, and post-and-edit streaming calls it once per
delta, so without this a single streamed reply would call the handler
back repeatedly on its own message
- **`previousMessage` is forwarded on edits.** slack sends the pre-edit
message and it was being dropped. an edit handler usually needs the
before to know what changed, so it is the optional third argument
- **the two shapes differ deliberately.** an edit carries a full
replacement message, so it gets `(thread, message, previousMessage?)`. a
delete has no message, only the id of what was removed, so it gets an
event. use `chat.thread(event.threadId)` when a delete handler needs one
- **one thread id helper** now serves message, edit, and delete, so an
edit cannot resolve to a different thread than the message it edits

## test plan

core:

- an edit dispatches to `onMessageUpdated` and not to the normal message
handlers
- the handler receives the pre-edit message as its third argument
- the bot's own edits are skipped
- a delete dispatches with normalized event data
- both run inside the active conversation, so read tools built in these
handlers stay scoped

slack:

- `message_changed` dispatches as an update, `message_deleted` as a
delete
- `previous_message` is forwarded, and left undefined when slack omits
it
- hidden unfurl updates stay ignored, hidden real edits still dispatch
- message, edit, and delete resolve to one thread id in a flat DM and in
a threaded `agent_view` DM

verified against a real slack workspace over socket mode: editing and
deleting a DM both routed to the same thread id as the original message

---------

Co-authored-by: Miłosz Lenczewski <m.lenczewski@tidio.net>
2026-08-05 13:22:54 +01:00