101 Commits

Author SHA1 Message Date
‌ 31bce0a7a0 feat(whatsapp): expose typed API errors (#896)
- export `WhatsAppApiError` so consumers can handle Meta error codes
without parsing error messages
- expose HTTP status, provider details, optional subcode and trace ID,
and the raw response
- cover message requests, media uploads, and media metadata failures
while preserving existing error messages and `AdapterError`
compatibility
- add regression coverage and document error handling

closes #712

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-09-05 05:53:12 +00:00
‌ 7062c395d0 fix(teams): preserve outgoing mention text (#898)
- keep outgoing `@names` as plain text instead of generating `<at>`
markup without the mention entities Teams requires
- preserve multi-word names across plain text, raw, markdown, and AST
messages
- keep incoming mention decoding and explicit raw markup unchanged
- add formatter and send/edit regression tests and document that
plain-text names do not notify users

closes #853
2026-09-05 15:23:02 +10:00
‌ aaeede70be feat(teams): dispatch bot join events (#899)
- dispatch `onMemberJoinedChannel` when the bot joins a Teams channel or
group chat
- expose `botUserId` from the configured app identity
- preserve channel routing, inviter identity, and webhook `waitUntil`
tracking
- add regression tests and document the bot-only scope

addresses the bot-join portion of #847; personal install/uninstall hooks
remain separate

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-09-05 05:21:54 +00:00
Hiroki Osame 2cc8cc3f80 fix(slack): surface custom status text in the Agent messaging experience (#897)
## summary

- restore custom loading labels for `startTyping` and
`setAssistantStatus` under `agentView`, which stopped displaying after
#862 moved status updates to the native sessions API
- send custom labels through `assistant.threads.setStatus` with
`loading_messages`; `setAssistantStatus` preserves explicit arrays, then
configured defaults, then falls back to the custom status
- keep native `processing` and initiator attribution when
`startTyping()` has no custom status, and use native `active` when
clearing
- add regression coverage for message precedence, native routing,
clearing, and native API failures
- verify custom labels in DMs and channels, streamed completion, and
real native stop-button cancellation against locally built packages

## limitations

custom labels and native session state are not equivalent: in the test
workspace, the custom-label path displayed the requested text but did
not create a native processing session or stop button, even with
`agent_session_stopped` enabled

use `startTyping()` without custom text when native processing and stop
behavior are required; an existing native processing indicator can also
take precedence over a custom label

Slack's [native sessions API](https://docs.slack.dev/ai/agent-sessions/)
does not accept custom loading text, so this restores labels through the
[legacy status
endpoint](https://docs.slack.dev/reference/methods/assistant.threads.setStatus/)
without promising identical lifecycle behavior

---------

Co-authored-by: dancer <josh@afterima.ge>
2026-09-04 21:40:02 +00:00
psychomet 043386b52c feat(telegram): add Business mode support (#888)
- Adds Telegram Business mode support to `@chat-adapter/telegram`
- Handles `business_connection`, `business_message`, and
`edited_business_message` updates
- Passes `business_connection_id` on outbound sends, edits, typing, and
file uploads
- Opt-in via `businessMode: true` (default off, backward compatible)

Closes #887

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-09-04 04:03:01 +00:00
dcbuilder.eth d4a1f03afc fix(slack): rotate long native streams before expiry (#884)
Slack expires native streams after roughly five minutes. Finalize
long-running streams after four minutes by default and continue in a
fresh segment so late appends do not fail with
message_not_in_streaming_state. Preserve open fenced code blocks by
closing and reopening them across the segment boundary. The threshold is
configurable with streamSegmentMaxAgeMs.

---------

Signed-off-by: dcbuild3r <dcbuilder@pm.me>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-09-04 10:54:00 +10:00
Ben Sabic f485255bcf fix(adapters): harden webhook tenant isolation (#877)
Multi-workspace Slack now ignores commands and interactions when their
installation cannot be found, and channel names stay isolated per
workspace.

Google Chat no longer learns its identity from incoming mentions, and
forward history reads use bounded native pagination.

Webhook logs avoid message content. The example app protects preview
routing, records only successful verified deliveries, and caps recording
size and retention.

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Signed-off-by: dancer <josh@afterima.ge>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: dancer <josh@afterima.ge>
2026-09-03 17:30:32 +01:00
Max a8de95bcc4 fix(teams): infer missing conversation types (#879)
## Summary

Teams can omit `conversationType` while still sending
`conversation.isGroup`. Use `isGroup` and team context as a fallback so
`a:`-prefixed group chats are not treated as DMs. An explicit
`conversationType` still takes precedence.

## Test plan

- `pnpm validate`
- `pnpm --filter @chat-adapter/teams test`, 276 tests passed

## Checklist

- [x] All commits are signed and verified
- [x] All commits are signed off for the DCO with `git commit -s`
- [x] `pnpm validate` passes
- [x] Changeset added
- [x] Documentation updated

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-09-04 00:25:29 +10:00
Ben Sabic 7609d8f60e fix(adapters): validate external request targets (#876)
Adapters now reject untrusted destinations before sending credentials,
message content, or attachment requests.

Teams Connector and Graph calls stay on known Microsoft hosts, Instagram
downloads stay on trusted Meta hosts, and Slack response URLs are
checked before use.

XChat now handles CRC challenges itself and rejects tokens that could be
reused to forge webhook signatures.

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-31 17:13:03 +00:00
Noppakorn Kaewsalabnil f691ad5848 docs: add LINE community adapter (#873)
## Summary

- Add `chat-adapter-line` to the community adapter catalog.
- Add a hand-authored LINE adapter docs page with configuration,
webhook, messaging, and feature-matrix details.
- Register `chat-adapter-line` as a valid docs code-example import.

## Validation

- `pnpm --filter @chat-adapter/integration-tests test --
src/docs-adapters.test.ts --coverage=false` — 467 tests passed
- `pnpm --filter @chat-adapter/integration-tests test --
src/docs-content.test.ts --coverage=false` — 91 tests passed
- `pnpm --filter @chat-adapter/integration-tests test --
src/docs-llms.test.ts --coverage=false` — 145 tests passed
- `pnpm exec biome check apps/docs/content/adapters/community/line.mdx
apps/docs/content/adapters/community/meta.json apps/docs/adapters.json
packages/integration-tests/src/documentation-test-utils.ts` — passed

Signed-off-by: PunGrumpy <108584943+PunGrumpy@users.noreply.github.com>
2026-08-30 18:42:17 +10:00
Ben Sabic 894fc7c7b3 docs: redirect conversation-history and update Vercel Connect page (#870)
Redirects /docs/conversation-history to /docs/history, since the History
guide already covers the old transcripts content. Removes the Vercel
Connect beta callout and adds related links to the Chat SDK docs and The
Complete Guide to Vercel Connect.

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 10:31:31 -07: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
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 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 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 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
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 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 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 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
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
josh 7a1922357c fix(gchat): bind add-on webhook verification to a configured identity (#787)
## summary

endpoint-URL webhook verification accepted any `email` claim matching
the generic Workspace Add-on shape:

```ts
/^service-\d+@gcp-sa-gsuiteaddons\.iam\.gserviceaccount\.com$/
```

the `\d+` is a GCP project number, and service agents are
`service-{PROJECT_NUMBER}@gcp-sa-{SERVICE}...` for the project that owns
them. so that shape identifies "some Workspace Add-on", not *this* app's
add-on, and it was the only thing standing between a public endpoint URL
and a verified request. the method's own doc comment already stated the
correct invariant, that the token is only trustworthy if it was issued
to Google Chat itself

adds `workspaceAddOnServiceAccountEmail` (env
`GOOGLE_CHAT_WORKSPACE_ADDON_SERVICE_ACCOUNT_EMAIL`) and compares add-on
identities exactly. when it is unset, add-on-shaped tokens are rejected
rather than trusted by shape, with a log naming the option to set

`chat@system.gserviceaccount.com` is untouched, so standalone Chat apps
behave exactly as before. the project-number and Pub/Sub paths were
already bound to exact identities and are unchanged

### behavior

| token `email` | before | after |
| --- | --- | --- |
| `chat@system.gserviceaccount.com` | accept | accept |
| add-on shape, matches configured identity | accept | accept |
| add-on shape, different project | accept | **reject** |
| add-on shape, option unset | accept | **reject** |

<details>
<summary>why not reject at construction</summary>

refusing to initialize when the option is absent would be the
stricter-looking choice, but the adapter cannot tell Workspace Add-on
mode from config alone, it only sees `endpointUrl`. throwing there would
break every ordinary endpoint-URL Chat app. rejecting add-on-shaped
tokens at verification is the precise equivalent without the collateral

</details>

## test plan

- an add-on token matching the configured identity is accepted
- an add-on token from a different project is rejected, the case the
generic shape allowed
- an add-on token is rejected when no identity is configured
- `chat@system.gserviceaccount.com` is still accepted with no add-on
config
- suffixed and prefixed lookalike domains, an uppercase variant, and
trailing whitespace are all rejected
- a matching identity with `email_verified: false` is rejected

the two rejection cases above returned 200 before this change and 401
after
2026-08-05 13:16:07 +01:00
Ben Sabic 258a7312ba docs: add vendor-official guide and refresh adapter docs (#784)
Adds a vendor-official contributing guide covering qualifications,
listing terms, and the PR checklist for platform vendors. Contributing
and adapter overview pages point to that guide for listing details
instead of repeating them.

Moves Slack and Teams low-level API docs onto their adapter pages, with
permanent redirects from `/docs/slack-primitives` and
`/docs/teams-primitives`.

Trims stale hand-maintained comparison tables from the docs intro and
platform adapters overview. Those pages now link to `/adapters` and the
generated official feature matrix. Adds contributing CTAs for building
an adapter and listing a vendor-official one.

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-05 14:08:46 +10:00
Ben Sabic fe4ed11ea9 docs: add XChat branding and clarify X vs XChat adapters (#777)
- Add a dedicated XChat speech-bubble logo for the docs hero and
`/adapters` card
- Point XChat docs and `adapters.json` at the new `xchat` icon instead
of reusing `x`
- Update the XChat OG image
- Add reciprocal “X Adapter vs XChat Adapter” / “XChat Adapter vs X
Adapter” sections on both docs pages
- Rename remaining “X Chat” references to “XChat” in the adapter package
README and comments

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-03 23:53:24 +10:00
Aradhya C P 0642ce335f docs: document WhatsApp typing indicator support (#772)
This PR updates the WhatsApp adapter documentation to reflect the
existing typing indicator support through `thread.startTyping()`.

The feature was already implemented in the adapter but was missing from
the documentation and feature matrix, making it difficult for users to
discover.

Fixes #771
2026-08-03 14:26:14 +10:00
Max 629e655578 fix(telegram): combine incoming media groups (#760)
- buffer incoming Telegram updates that share a `media_group_id` and
dispatch them once the album settles
- coordinate through the configured `StateAdapter` so separate
serverless instances still produce one message
- preserve the shared caption and order attachments by Telegram message
ID

---------

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-08-01 11:52:04 +10:00
Santiago Medina caa63253c5 feat(x): add XChat encrypted messaging support (#745)
## summary

new `@chat-adapter/xchat` adapter for XChat, X's encrypted messaging.
write bot logic once and hold encrypted 1:1 and group conversations like
the other Chat SDK adapters — all crypto handled inside the adapter via
`@xdevplatform/chat-xdk` (wasm), all REST via the typed
`@xdevplatform/xdk` client.

## background: chat-xdk


[`@xdevplatform/chat-xdk`](https://www.npmjs.com/package/@xdevplatform/chat-xdk)
is the official XChat cryptography SDK — a Rust core compiled to
WebAssembly that implements the XChat encryption protocol. it handles
per-conversation symmetric keys and key exchange, message
encryption/decryption, event signing and signature verification, and
encrypted media (secretstream). the bot's private keys live in a
PIN-protected [Juicebox](https://juicebox.xyz) store (secret-shared
across independent realms), so no key material sits in env vars or on
disk — the adapter unlocks with a PIN at startup. this adapter is the
glue: chat-xdk produces and consumes the encrypted envelopes, the typed
`@xdevplatform/xdk` client moves them over the X API, and everything is
normalized to the Chat SDK's `Thread`/`Message` model.

what it supports:
- encrypted send/receive in DMs and groups (webhook push + polling),
signature verification on by default
- mention detection from structured mention entities, swipe-replies to
the bot, and a plain-text `@handle` fallback; group replies go out as
quoted replies with TTL propagated
- `openDM(userId)`: starts (or reuses) an encrypted 1:1 —
cached/history-recovered conversation key, else a full key exchange so
the bot can message first
- media both ways: inbound attachments with lazy download+decrypt,
outbound encrypted (secretstream) via the 3-step upload flow
- edit and delete of the bot's own messages: edits are encrypted events
targeting the original's sequence id; deletes are locally signed
delete-for-all actions recipients verify
- reactions in and out, typing keep-alive while handlers run,
configurable group welcome message
- read receipts sent per delivered inbound message (`sendReadReceipts`,
default on)
- cards by degradation: text + tappable entities, link buttons as
`label: url` lines, primary link as a URL preview attachment with
optional encrypted banner

key design decisions:
- mdast stays the canonical format; markdown passes through as raw text
(XChat clients render plain text — no markdown), with URLs and @mentions
made tappable via entity spans and tables degraded to ASCII code blocks
- thread ids are `xchat:{conversationId}` (groups `g…`, 1:1s the sorted
participant pair)
- the first edit of a fresh message is age-gated (`editSafetyDelayMs`,
default 5000ms): receiving clients park an edit whose original hasn't
arrived, leaving the message permanently invisible — the gate prevents
that race
- undecryptable or unverified events are dropped, never delivered as
empty messages
- no core changes: the adapter implements the standard `Adapter`
interface only

also includes the `chat/adapters` catalog entry, docs page (with OG
image), `adapters.json` registry entry, and `create-chat-sdk` scaffold
spec, modeled on the `x` adapter's registration.

<details><summary>usage</summary>

```bash
XCHAT_BOT_TOKEN=...    # OAuth2 user access token (identity resolved from GET /2/users/me)
XCHAT_PIN=...          # Juicebox PIN that unlocks the bot's keys
X_CONSUMER_SECRET=...  # optional: verifies webhook signatures
```

```typescript
import { Chat } from "chat";
import { createXchatAdapter } from "@chat-adapter/xchat";
import { createMemoryState } from "@chat-adapter/state-memory";

const bot = new Chat({
  userName: "mybot",
  adapters: { xchat: createXchatAdapter() }, // credentials from env
  state: createMemoryState(),
});

// DMs always
bot.onDirectMessage(async (thread, message) => {
  await thread.post(`You said: ${message.text}`);
});

// group chats when the bot is @mentioned
bot.onNewMention(async (thread, message) => {
  await thread.post("You rang?");
});

// wire the webhook (e.g. a Next.js route)
export async function POST(request: Request) {
  return bot.webhooks.xchat(request);
}
```

</details>

testing: 109 unit tests, including real-wasm-crypto round trips against
vendored fixture vectors (decrypt + signature verification, webhook
delivery, read receipts, edit age-gating, signed deletes). verified live
against production XChat: DMs, group mentions, media, reactions, edits,
deletes, openDM, cards.

note on the lockfile: `@xdevplatform/xdk@0.6.6` was published <48h ago,
so it was resolved with a one-shot `--config.minimumReleaseAge=0`
override; the locked integrity hash was verified against the npm
registry. the repo policy file is untouched.

---------

Co-authored-by: dancer <josh@afterima.ge>
2026-07-31 23:52:18 +01:00
Max 53bf73db22 fix(telegram): preserve media identity and MIME metadata (#752)
## Summary

Preserve Telegram's stable `file_unique_id` alongside the current
downloadable `file_id` in normalized attachment metadata. Telegram photo
attachments now report `image/jpeg`, including rich-message photos.

This keeps adapter normalization in `@chat-adapter/telegram`;
deduplication remains consumer-owned. After upgrading to the release
containing this patch, Calories can remove
`patches/@chat-adapter__telegram@4.35.0.patch` while retaining its
perceptual-hash fallback for recompressed images.

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-07-31 17:49:26 +01:00
Max 3c37cfbc15 fix(teams): authenticate protected inline attachments (#749)
## Summary

Authenticates connector-hosted Teams inline attachments through the
configured Bot Framework client, so `message.attachments[].fetchData()`
can retrieve protected content. Teams file download cards use their
direct `content.downloadUrl` anonymously, and serialized attachments
reconstruct the same routing during rehydration.

Bot credentials are limited to non-redirecting HTTPS requests whose
origin exactly matches the Activity connector origin. Cross-origin URLs,
HTTP URLs, and file-card download URLs never enter the authenticated
client path.

File-card MIME inference covers common image and text formats plus PDF,
XLS, and XLSX attachments.

The implementation follows [Microsoft's inline-image access-token
sample](https://learn.microsoft.com/en-us/samples/officedev/microsoft-teams-samples/officedev-microsoft-teams-samples-bot-file-upload-nodejs/)
and [Teams file-card
contract](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-filesv4).


Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-07-31 17:36:36 +01:00
Demo Macro a8867a0abb docs(adapters): add QQ Bot and WeCom community adapters (#753)
Adds two community platform adapters to the docs and `adapters.json`
registry:

- `@agentor/chat-qq` — QQ Bot. WebSocket or webhook (Ed25519) modes; QQ
DM,
  group, and text-channel scenes; rich media.
- `@agentor/chat-wecom` — WeCom (企业微信). Group webhook bots, smart bots
  (callback or WebSocket), and apps; WeCom Template Cards (5 types) and
  AES-256-CBC callback encryption.

Follows the community-adapter flow: docs MDX pages, `meta.json`,
`adapters.json` entries, and `VALID_DOC_PACKAGES`. Community-only
adapters
intentionally omit a `chat/adapters` catalog entry, a `create-chat-sdk`
scaffold-spec, and a changeset.

Signed-off-by: Demo Macro <abc@imst.xyz>
2026-07-31 18:14:05 +10:00
Max 257a32d01c fix(teams): classify group chats by conversation type (#746)
Fixes `a:`-prefixed Teams group chats being treated as DMs, so they use
non-DM routing and buffered responses.

[Microsoft defines conversation IDs as opaque strings and
`conversationType` as the channel-provided conversation
discriminator](https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#conversation-account).
The adapter therefore stores a classification override only when
`conversationType` disagrees with the legacy ID-prefix heuristic.
Existing subscription and history keys remain stable when the heuristic
was already correct. Explicit group chats also bypass stale DM Graph
context, and Graph-listed child threads retain the override.

---------

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-07-28 00:34:29 +10:00
Max 54eea71501 feat(telegram): add user allowlist (#742)
## Summary

Add an opt-in `allowedUserIds` Telegram adapter option, with
`TELEGRAM_ALLOWED_USER_IDS` as a comma-separated environment fallback.
Updates from other or unidentified users are ignored before dispatch.

This follows the adapter-level targeting pattern from [the Discord
channel response allowlist](https://github.com/vercel/chat/pull/715),
while enforcing an ingress allowlist instead of expanding mention
routing.

## Test plan

- `pnpm --filter @chat-adapter/telegram test`
- `pnpm --filter @chat-adapter/telegram typecheck`
- `pnpm check`
- `pnpm konsistent`
- `TURBO_CONCURRENCY=2 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: onmax <maximogarciamtnez@gmail.com>
2026-07-24 21:06:44 +01:00
Aamir Jawaid 160140e32b feat(teams): add targeted ephemeral messages (#737)
## Summary

Microsoft Teams supports targeted messages that are visible only to a
selected conversation member, but the Teams adapter did not expose that
native behavior through the SDK's ephemeral-message API. This PR wires
`postEphemeral` for Teams to send native targeted messages while
preserving normal `postMessage` behavior by default.

The adapter now creates explicit targeted outbound activities with
`MessageActivity.withRecipient(recipient, true)` for text and
adaptive-card messages, returns `usedFallback: false`, and keeps the
feature gated behind `thread.postEphemeral()` /
`channel.postEphemeral()`. It also bumps the Teams SDK packages to
`^2.0.13`, adds targeted coverage, updates public docs/matrices, and
includes a changeset.

Live verification found that Teams targeted messages require the app to
be installed in the shared conversation. Group chats and channels both
worked after using the Teams install picker with `Open -> select
placement -> Go`; personal bot chat targeted sends returned a Teams
`BadArgument` response.

## Test plan

Previously validated with:

- `corepack pnpm --filter @chat-adapter/teams exec vitest run
src/index.test.ts --coverage.enabled=false`
- `corepack pnpm --filter @chat-adapter/teams exec tsc --noEmit`
- `corepack pnpm --filter example-nextjs-chat exec tsc --noEmit`
- `corepack pnpm --filter chat exec vitest run src/emoji.test.ts
--coverage.enabled=false`
- `corepack pnpm --filter @chat-adapter/teams exec tsup`
- Targeted `ultracite check` on changed files

Live verified `TeamsAdapter.postEphemeral(...)` in:

- Group chat `Demo Test 2`: Teams UI showed `Only you can see this
message`.
- Channel `General / Teams SDK`: Teams returned message ID
`1784749118197`, and the UI showed `Only you can see this message`.

<img width="884" height="299" alt="Screenshot 2026-07-22 at 12 41 41 PM"
src="https://github.com/user-attachments/assets/cd350ac8-c158-4779-8028-3450eb8670f2"
/>
<img width="1098" height="559" alt="Screenshot 2026-07-22 at 12 41
33 PM"
src="https://github.com/user-attachments/assets/bf6ea12b-ab11-46ee-a3a8-ff5e9583066d"
/>


## Checklist

- [ ] All commits are signed and verified - unsigned commit created
after local GPG/SSH signing was unavailable and user approved continuing
- [ ] `pnpm validate` passes - full validate not run; targeted
validation listed above
- [x] Changeset added (or N/A - see
[CONTRIBUTING.md](./CONTRIBUTING.md))
- [x] Documentation updated (or N/A)

---------

Co-authored-by: dancer <josh@afterima.ge>
Copilot-Session: 601a7414-48f0-4e6d-ba03-28fa4d2d5c0a
2026-07-23 19:53:13 +01:00
Aradhya C P 09b72e9dd3 fix(whatsapp): stop duplicating card title when posting Card with files (#736)
## Description

Fixes a WhatsApp adapter bug where posting a **Card together with
files** caused the card title (and other card text) to appear twice:
once in the media caption (from `cardToFallbackText`) and again in the
interactive message header/body (from `cardToWhatsApp`).

`postMessageWithMedia` now checks whether the card will be sent as an
interactive message first. If so, it skips using the full card fallback
as the media caption and lets the interactive message own the title,
body, and buttons. Text-fallback cards + files keep the previous caption
behavior (single message, no duplicate text).

## Type of Change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Refactoring (no functional changes)

## Related Issues

Fixes #735
Closes #735
Related to #735

<!-- Replace # with the issue number after opening the bug report -->

## Changes Made

- In `postMessageWithMedia`, compute `cardToWhatsApp(card)` first and
reuse that result.
- When the card is **interactive**, do not set media caption from
`cardToFallbackText` (empty caption text) so title/body are not
duplicated on the image.
- When the card is **text fallback**, keep captioning media with
`cardToFallbackText` and avoid sending a second text message (existing
behavior).
- Expand unit coverage in `index.test.ts` for interactive + files (title
once, no caption duplication for text/fields, multi-file, audio, HTTPS
attachment) and text-fallback caption behavior.

## Testing

- [x] All existing tests pass
- [x] Added new tests for the changes
- [x] Manually tested the changes

### Test Coverage

- Built the package, then used `pnpm link` to link the built `dist` into
a separate test project.
- Exercised `thread.post({ card, files })` against the reported bug
scenario and confirmed the title no longer appears twice (caption empty
for interactive cards; title only on the interactive message).
- Added / updated unit tests in
`packages/adapter-whatsapp/src/index.test.ts`; all related tests pass
locally (`pnpm --filter @chat-adapter/whatsapp test`).

## Screenshots/Demos

<!-- Paste before/after WhatsApp screenshots here -->

**Before (title duplicated on caption + interactive header):**

<img width="433" height="428" alt="image"
src="https://github.com/user-attachments/assets/37c776fd-b4f6-48f3-a365-6e2073316576"
/>



**After (title only on interactive message; media uncaptioned):**

<img width="428" height="390" alt="image"
src="https://github.com/user-attachments/assets/fb1f43d9-623a-4e22-83ea-dd96cd6d3877"
/>

<img width="408" height="405" alt="image"
src="https://github.com/user-attachments/assets/3959eb04-e722-43f4-8b85-91b724faebcb"
/>


## Checklist

- [x] My code follows the project's code style
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings or errors
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have created a changeset (`pnpm changeset`)
- [x] All commits are signed and verified
- [ ] 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))
- [ ] Documentation updated (or N/A)

## Changeset

- [x] I have created a changeset for these changes

<!-- Reminder: behavioural package changes need `pnpm changeset` for
`@chat-adapter/whatsapp` -->

## Additional Notes

- No change to `@chat-adapter/shared`’s `cardToFallbackText` — it
remains correct as a full text fallback. The bug was reusing that full
fallback as a caption while also sending a full interactive card.
- Card-only posts (no files) are unchanged.

---------
2026-07-23 19:18:25 +01:00
Utopia 5eb8b846a7 feat(teams): support outbound reactions (#734)
Outbound Teams reactions were originally implemented as part of #302,
then removed because the Teams feature was not fully rolled out. In [the
follow-up
discussion](https://github.com/vercel/chat/pull/302#issuecomment-4147056867),
the Teams SDK maintainer said they were happy to add the support back
once the rollout was ready. Microsoft now documents agent reaction
support without a preview caveat.

This PR restores that support against the current Teams SDK API:

- implement `addReaction` and `removeReaction` with
`conversations.addReaction` / `conversations.deleteReaction`
- pass native Teams reaction IDs through unchanged and map common
normalized Chat SDK emoji names to their Teams IDs
- upgrade the aligned `@microsoft/teams.*` dependencies to 2.0.14
- update the Teams feature matrices and add a minor changeset

The implementation stays within the existing adapter methods and does
not add another abstraction or affect streaming behavior.

---------

Signed-off-by: Utopia <154325211+Utopi-a@users.noreply.github.com>
2026-07-23 10:52:00 +10:00