mirror of
https://github.com/vercel/chat.git
synced 2026-09-14 18:32:29 +08:00
@chat-adapter/twilio@4.36.0
517 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
470b6af94b |
chore(release): version packages (#748)
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/slack@4.36.0 ### Minor Changes - |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
b547f45842 |
fix(chat): stop treating email addresses as bot mentions (#761)
## summary a message containing an email address like `jane@acme.com` was treated as a mention of a bot named `acme`, so the bot engaged on every pasted colleague email `detectMention` now requires the `@` not to follow a word character: ```ts `(?<!\\w)@${escapeRegex(botUserName)}(?![\\w-])` ``` - applies to both the username and the user id pattern - an email local part always ends in a word character, so `jane@acme.com`, `foo.bar@acme.com`, `foo-bar@acme.com` and url userinfo are all excluded - real mentions are unaffected: start of a message, after a space, after punctuation like `(` or `:`, and after an ellipsis fixes #759 |
||
|
|
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> |
||
|
|
0153a39f7b |
feat(modals): add DateInput and NumberInput modal children (#757)
`ModalChild` is `TextInput | Select | ExternalSelect | RadioSelect | Text | Fields` — there is no date or number primitive. A bot collecting a renewal date or a quantity has to render a text input with a `YYYY-MM-DD` hint and validate the string on submit, on every platform, even though neither platform is the constraint: - Slack Block Kit has a native [`datepicker`](https://docs.slack.dev/reference/block-kit/block-elements/date-picker-element) and [`number_input`](https://docs.slack.dev/reference/block-kit/block-elements/number-input-element). - Adaptive Cards has `Input.Date` and `Input.Number`, both already exported by `@microsoft/teams.cards`. One addition to the union lifts both surfaces. `ModalSubmitEvent.values` stays `Record<string, string>`, so this is additive for existing handlers. ## Changes - `DateInput` / `NumberInput` element types, builders, and options in `packages/chat/src/modals.ts`, added to `ModalChild` + `VALID_MODAL_CHILD_TYPES`. - JSX/React support: props, component overloads, `modalComponentMap`, and `fromReactModalElement` branches. - Slack renderer: `datepicker` (`initial_date`, `placeholder`) and `number_input` (`is_decimal_allowed`, `initial_value`, `min_value`, `max_value` — Slack takes these as strings). - Teams renderer: `Input.Date` / `Input.Number`, in both `modals.ts` (`@microsoft/teams.cards`) and the dependency-free `modals-primitives`. - Docs: `docs/modals.mdx` component tables and `docs/api/modals.mdx` reference + `ModalChild` table. - Changeset (`chat`, `@chat-adapter/slack`, `@chat-adapter/teams`: minor). ### Two runtime decode gaps this had to close - Slack reports a datepicker as `selected_date`, not `value`, so view-submission flattening now reads `value ?? selected_date ?? selected_option?.value`. `number_input` already arrives as `value`. - Teams' `Input.Number` submits a JSON **number**, which the previous `typeof val === "string"` filter dropped silently. Numbers are now stringified in both `parseDialogSubmitValues` and `parseTeamsDialogSubmitValues`. This applies to any numeric value a Teams dialog submits, not only `Input.Number` — a key that used to be absent from `event.values` is now present as a string. Called out in the changeset; one existing test updated. Non-scalar values are still dropped. ### Deliberate asymmetries - `DateInput` has no `min`/`max` — Slack's `datepicker` has no bounds, and a prop that silently does nothing on one platform is worse than its absence. - `NumberInput.decimal` maps to Slack's required `is_decimal_allowed`. Adaptive Cards has no decimal switch, so Teams accepts decimals either way; this is called out in the docs. - A `DateInput` `initialValue` that is not a real `YYYY-MM-DD` date is dropped with a warning instead of forwarded. Slack rejects a malformed `initial_date` by failing the entire `views.open` with `invalid_arguments` — the modal never opens, and the error surfaces as a JSON pointer rather than anything actionable. Adaptive Cards just renders the field empty, so forwarding verbatim would make the same modal work on Teams and die on Slack. The drop matches how `filterModalChildren` handles unsupported children. Validation round-trips through `Date` because it rolls impossible dates over (`2026-02-31` → Mar 3) instead of rejecting them. Signed-off-by: CamdenA21 <camden@sandstone.com> |
||
|
|
c5d86b103e |
feat(chat): scope agent read tools to the active conversation (#751)
built-in agent read tools now stay inside the conversation they are handling, so a thread or channel id the model supplies that resolves elsewhere is rejected before the adapter is called - `Chat` tracks the conversation being handled across message, action, slash command and reaction dispatch, using `AsyncLocalStorage` - read tools (`fetchMessages`, `fetchChannelMessages`, `fetchThread`, `listThreads`, `getThreadParticipants`, `getChannelInfo`) inherit that conversation, so existing handlers get this with no code change - scoping is per channel, so an agent can still follow other threads in its own conversation - pass `scope` to set it explicitly, or `scope: false` for workspace-wide reads this brings read tools in line with the least-privilege defaults write tools already have, where `needsApproval` is on unless you opt out **not breaking:** `scope` is optional and every existing call site keeps working. agents that run outside a handler, such as a queued job or a resumed workflow step, have no conversation to inherit and should pass `scope` ## test plan - read tools reject out-of-conversation ids and still serve in-conversation ones, per tool - the conversation is inherited correctly through message, action, slash command and reaction dispatch - concurrent conversations stay isolated, so one agent cannot inherit another's scope - an explicit `scope` overrides the handled conversation, and `scope: false` restores workspace-wide reads - ids resolve through `adapter.channelIdFromThreadId`, verified against the slack, teams, google chat, discord, telegram and whatsapp id shapes - `pnpm validate` clean: workspace tests, typecheck, lint, knip and build all pass |
||
|
|
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> |
||
|
|
e3c136b6dc |
chore(release): version packages (#710)
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.35.0 ### Minor Changes - |
||
|
|
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> |
||
|
|
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 |
||
|
|
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. --------- |
||
|
|
92530dd352 |
fix(slack): return replyable thread ID from channel posts (#720)
## summary fixes #719 updates Slack channel posts to return a replyable thread ID rooted at the newly posted top-level message `postChannelMessage` still omits `thread_ts` for the initial channel post, then uses Slack's returned message `ts` to construct `slack:<channel>:<ts>` for the `SentMessage` this allows `chat.thread(sent.threadId).post(...)` to reply to a message returned by `channel.post(...)` instead of creating another top-level channel message file-only uploads without a Slack message `ts` keep their existing channel-scoped thread ID ## test plan - added a Slack adapter regression assertion that `postChannelMessage` returns `slack:C123:<message ts>` |
||
|
|
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> |
||
|
|
25f30998ce |
fix(chat): keep attachment/link-only messages in toAiMessages (#713)
- `toAiMessages` previously filtered out any message with empty or whitespace-only text (`sorted.filter((msg) => msg.text.trim())`). This discarded messages that carry meaningful content without text — e.g. an image uploaded with no caption, a file-only upload, or a link-only message. - Now messages are kept as long as they have usable content (text, image/file attachments, or links). Only messages with *none* of those are skipped. - When an attachment-only message is included, no empty `text` part is prepended (an empty text part would be rejected by the AI SDK). Link-only messages render a standalone `Links:\n...` block. ## Changes - `packages/chat/src/ai/messages.ts` — drop the text-only pre-filter; build text conditionally; skip only truly empty messages. - `packages/chat/src/ai/messages.test.ts` — add tests for image-only, link-only, interleaved, whitespace-with-attachment, and fully-empty cases. - `apps/docs/content/docs/ai/to-ai-messages.mdx` — update the documented filtering behavior. - Changeset added (`chat`: patch). --------- Co-authored-by: Cole Corrente <cole.corrente@snowflake.com> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
e06b4b606b |
feat(teams): forward a custom token factory to the Teams SDK (#732)
`TeamsAdapterConfig` never forwards a `token` field through to the underlying `@microsoft/teams.apps` `AppOptions.token`, even though the Teams SDK already supports it as a genuine "bring your own credentials" escape hatch (`TokenCredentials['token']`). The only non-secret auth path currently exposed is `federated`, which maps to `managedIdentityClientId` and only resolves via Azure-native managed-identity sources (IMDS, AppService, CloudShell, MachineLearning, ServiceFabric). That's unreachable from serverless/edge runtimes (e.g. Vercel) that can't hit Azure IMDS but still need to mint access tokens through an external mechanism (e.g. a workload-identity federation bridge exchanging a platform-native OIDC token for an Azure AD token). We've been carrying a local patch on `@chat-adapter/teams` doing exactly this forwarding to unblock a production Teams bot running on Vercel with a user-assigned managed identity. Opening this as a proper PR instead of staying on the patch indefinitely. ## Changes - `TeamsAdapterConfig.token?: (scope: string | string[], tenantId?: string) => string | Promise<string>` — matches `TokenCredentials['token']`'s real signature. - `toAppOptions` forwards `config.token` straight through. - `clientSecret` resolution now also short-circuits when `token` is provided (alongside the existing `federated` check) — `TokenManager.initializeCredentials` checks `clientId && clientSecret` before `clientId && token`, so a stray `appPassword`/`TEAMS_APP_PASSWORD` would otherwise silently win over an explicitly configured token factory. - Unit tests in `config.test.ts` and a `createTeamsAdapter` factory test in `index.test.ts`. - Docs: added the `token` config option and a third "Authentication methods" example in `apps/docs/content/adapters/official/teams.mdx`. - Changeset (`@chat-adapter/teams`: minor). --------- Signed-off-by: CamdenA21 <camden@sandstone.ai> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
b4a93bdbc5 |
build(deps-dev): bump @hono/node-server from 2.0.2 to 2.0.10 (#733)
Bumps [@hono/node-server](https://github.com/honojs/node-server) from 2.0.2 to 2.0.10. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/honojs/node-server/releases">@hono/node-server's releases</a>.</em></p> <blockquote> <h2>v2.0.10</h2> <h2>Security fixes</h2> <p>This release includes a fix for the following security issue:</p> <h3>Unauthenticated memory-leak DoS via aborted WebSocket handshake</h3> <p>Affects: <code>upgradeWebSocket</code>. A WebSocket upgrade request with a missing or malformed <code>Sec-WebSocket-Key</code> header leaked the request's <code>IncomingMessage</code> and left a promise pending, even though no connection was established. Since the route is reachable pre-handshake without authentication, an attacker could flood it to gradually exhaust memory. <a href="https://github.com/honojs/node-server/security/advisories/GHSA-9mqv-5hh9-4cgg">GHSA-9mqv-5hh9-4cgg</a></p> <hr /> <p>Users of <code>upgradeWebSocket</code> are encouraged to upgrade to this version.</p> <h2>v2.0.9</h2> <h2>What's Changed</h2> <ul> <li>fix(websocket): polyfill missing ErrorEvent global by <a href="https://github.com/otnc"><code>@otnc</code></a> in <a href="https://redirect.github.com/honojs/node-server/pull/371">honojs/node-server#371</a></li> <li>fix(serve-static): correct Range header parsing edge cases by <a href="https://github.com/otnc"><code>@otnc</code></a> in <a href="https://redirect.github.com/honojs/node-server/pull/372">honojs/node-server#372</a></li> <li>fix: recover complete request bodies after client disconnect by <a href="https://github.com/usualoma"><code>@usualoma</code></a> in <a href="https://redirect.github.com/honojs/node-server/pull/375">honojs/node-server#375</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/otnc"><code>@otnc</code></a> made their first contribution in <a href="https://redirect.github.com/honojs/node-server/pull/371">honojs/node-server#371</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/honojs/node-server/compare/v2.0.8...v2.0.9">https://github.com/honojs/node-server/compare/v2.0.8...v2.0.9</a></p> <h2>v2.0.8</h2> <h2>What's Changed</h2> <ul> <li>ci(release): add <code>--no-git-checks</code> option for <code>pnpm stage publish</code> by <a href="https://github.com/yusukebe"><code>@yusukebe</code></a> in <a href="https://redirect.github.com/honojs/node-server/pull/369">honojs/node-server#369</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/honojs/node-server/compare/v2.0.7...v2.0.8">https://github.com/honojs/node-server/compare/v2.0.7...v2.0.8</a></p> <h2>v2.0.7</h2> <h2>What's Changed</h2> <ul> <li>chore: migrate to pnpm by <a href="https://github.com/BlankParticle"><code>@BlankParticle</code></a> in <a href="https://redirect.github.com/honojs/node-server/pull/367">honojs/node-server#367</a></li> <li>fix(serve-static): serve precompressed files for application/octet-stream by <a href="https://github.com/yusukebe"><code>@yusukebe</code></a> in <a href="https://redirect.github.com/honojs/node-server/pull/366">honojs/node-server#366</a></li> <li>chore: bump <code>supertest</code> by <a href="https://github.com/yusukebe"><code>@yusukebe</code></a> in <a href="https://redirect.github.com/honojs/node-server/pull/368">honojs/node-server#368</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/honojs/node-server/compare/v2.0.6...v2.0.7">https://github.com/honojs/node-server/compare/v2.0.6...v2.0.7</a></p> <h2>v2.0.6</h2> <h2>What's Changed</h2> <ul> <li>ci: publish to npm from CI with OIDC trusted publishing and bump <code>np</code> by <a href="https://github.com/yusukebe"><code>@yusukebe</code></a> in <a href="https://redirect.github.com/honojs/node-server/pull/361">honojs/node-server#361</a></li> <li>ci: use npm Staged publishing by <a href="https://github.com/yusukebe"><code>@yusukebe</code></a> in <a href="https://redirect.github.com/honojs/node-server/pull/364">honojs/node-server#364</a></li> <li>fix: preserve status and statusText when cloning a Response with liveheaders by <a href="https://github.com/usualoma"><code>@usualoma</code></a> in <a href="https://redirect.github.com/honojs/node-server/pull/363">honojs/node-server#363</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/honojs/node-server/compare/v2.0.5...v2.0.6">https://github.com/honojs/node-server/compare/v2.0.5...v2.0.6</a></p> <h2>v2.0.5</h2> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/honojs/node-server/commit/7c1457ed5536c02fdd2f001129fae67bcbca54a1"><code>7c1457e</code></a> 2.0.10</li> <li><a href="https://github.com/honojs/node-server/commit/3a21938c418340e980cb7ffa88e78369f78392d1"><code>3a21938</code></a> Merge commit from fork</li> <li><a href="https://github.com/honojs/node-server/commit/98420217e53a17a238ef1aa1a6bef0b2b70136c5"><code>9842021</code></a> 2.0.9</li> <li><a href="https://github.com/honojs/node-server/commit/51f3bf56f56d9691ec0f7e1562a96f0b485a7dd9"><code>51f3bf5</code></a> fix: recover complete request bodies after client disconnect (<a href="https://redirect.github.com/honojs/node-server/issues/375">#375</a>)</li> <li><a href="https://github.com/honojs/node-server/commit/fdb87badbe313cfbfe6bb2355e9893dc0698d2bd"><code>fdb87ba</code></a> fix(serve-static): correct Range header parsing edge cases (<a href="https://redirect.github.com/honojs/node-server/issues/372">#372</a>)</li> <li><a href="https://github.com/honojs/node-server/commit/912e3fd80c4311756f724bd566de1433c8d772d9"><code>912e3fd</code></a> fix(websocket): polyfill missing ErrorEvent global (<a href="https://redirect.github.com/honojs/node-server/issues/371">#371</a>)</li> <li><a href="https://github.com/honojs/node-server/commit/114c15efb38dabaf81af774ddb764409e3d156d8"><code>114c15e</code></a> 2.0.8</li> <li><a href="https://github.com/honojs/node-server/commit/5db2d5df662cd69ff5c4cc23b8ecb3a6f63e4e38"><code>5db2d5d</code></a> ci(release): add <code>--no-git-checks</code> option for <code>pnpm stage publish</code> (<a href="https://redirect.github.com/honojs/node-server/issues/369">#369</a>)</li> <li><a href="https://github.com/honojs/node-server/commit/a528a77ed2c28dc12775c849abc6b6df6d4cb44c"><code>a528a77</code></a> 2.0.7</li> <li><a href="https://github.com/honojs/node-server/commit/b2d610c1e37a96639fbb2eae662e858800aa8906"><code>b2d610c</code></a> chore: bump <code>supertest</code> (<a href="https://redirect.github.com/honojs/node-server/issues/368">#368</a>)</li> <li>Additional commits viewable in <a href="https://github.com/honojs/node-server/compare/v2.0.2...v2.0.10">compare view</a></li> </ul> </details> <details> <summary>Maintainer changes</summary> <p>This version was pushed to npm by <a href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new releaser for <code>@hono/node-server</code> since your current version.</p> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/vercel/chat/network/alerts). </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
4cb7e5d58e |
feat(chat): durable human-in-the-loop approvals via chat/workflow (#728)
Adds a `chat/workflow` subpath export with `requestApproval()`. This is the DX from #284, rebuilt on Workflow SDK so the approval survives deploys, restarts, and arbitrarily long waits. No in-memory promises, no approvals registry, no restart-recovery machinery: the workflow suspends on a webhook and resumes when a button is clicked. `requestApproval()` posts a card with Approve/Deny buttons whose `callbackUrl` targets a `createWebhook()` URL, suspends the workflow until a decision (or optional durable-sleep timeout), validates approvers, finalizes the card in place with the outcome (removing the buttons, leaving an audit trail), and returns the decision. ```typescript import { requestApproval } from "chat/workflow"; import type { Thread } from "chat"; export async function deployApproval(opts: { thread: Thread; version: string }) { "use workflow"; const { approved, user, timedOut } = await requestApproval(opts.thread, { title: `Deploy ${opts.version}?`, fields: { Version: opts.version }, timeout: "24h", approvers: ["U_ALICE", "U_BOB"], }); if (approved) { await deploy(opts.version); } } ``` Starting it from a handler is one line. `Thread` instances serialize across the workflow boundary automatically via the existing `@workflow/serde` hooks on `ThreadImpl` (requires `chat.registerSingleton()`): ```typescript import { start } from "workflow/api"; bot.onNewMention(async (thread, message) => { await start(deployApproval, [{ thread, version: parseVersion(message.text) }]); }); ``` **Details** - `workflow` is a new **optional** peer dependency (same pattern as `ai`); the subpath is the only code that imports it - Unauthorized clicks (when `approvers` is set) and unrecognizable payloads post a notice / are ignored, and the workflow keeps waiting - On timeout the card is finalized as timed out and the result has `timedOut: true` - Card builders (`buildApprovalCard`, `buildResolvedCard`) are exported for custom flows - Verified the published `dist` preserves the `"use step"` directives and down-levels `using` correctly, so the app-side Workflow SDK compiler handles the library code - Docs page under Interactivity; changeset (`chat` minor); 8 unit tests mocking the `workflow` primitives **Deliberate deviation from #284:** no `thread.requestApproval()` method. The function must suspend at workflow level, so hanging it off `ThreadImpl` would make `workflow` a hard dependency of core (or require prototype patching). The standalone `requestApproval(thread, options)` keeps the dependency optional. --------- Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
907450d73f |
fix(slack): Enterprise Grid support (#724)
## Slack Enterprise Grid Support Fixes a set of Enterprise Grid gaps in the Slack adapter, found by auditing the adapter against Slack's current Enterprise documentation ([Developing for Enterprise orgs](https://docs.slack.dev/enterprise/developing-for-enterprise-orgs/), [Events API](https://docs.slack.dev/apis/events-api/), [oauth.v2.access](https://docs.slack.dev/reference/methods/oauth.v2.access/)). The headline bug: org-wide installs completing OAuth through `handleOAuthCallback` were stored under a key that webhook token resolution never looks up, so every subsequent event failed with "no installation found." ### Org-wide OAuth installs `oauth.v2.access` returns `team: null` and a populated `enterprise` object for org-wide installs (`is_enterprise_install: true`). The callback previously keyed every installation by `result.team.id`. It now keys org-wide installs by `enterprise.id` — the same key incoming webhooks resolve tokens by — and records `enterpriseId` / `isEnterpriseInstall` on `SlackInstallation`. The returned `teamId` is always the storage key, so it keeps round-tripping with `getInstallation` / `deleteInstallation` for both install types. ### Socket mode token resolution Socket-mode events (live and forwarded from a serverless listener) bypassed the multi-workspace token-resolution block in `handleWebhook` entirely and dropped `enterprise_id` / `is_enterprise_install` when rebuilding the payload. The resolution logic is now factored into helpers (`resolveEventRequestContext`, `runSlashCommand`, `extractInstallationFromInteractivePayload`) shared by both paths, so events, slash commands, and interactive payloads resolve per-installation tokens identically over HTTP and socket. Socket JSON delivers `is_enterprise_install` as a boolean where form-encoded webhooks deliver `"true"`; both shapes are handled. ### Event routing via `authorizations[0]` Slack documents the envelope's `authorizations[0]` — not the top-level fields — as the authoritative installation identity for an event. The top-level `team_id` / `enterprise_id` can name a different workspace for Slack Connect shared-channel events (slackapi/bolt-js#935), and org-wide envelopes may omit the top-level flags. Token resolution now prefers `authorizations[0]` with top-level fallback, matching Bolt's behavior. ### Installation-scoped user caches The user profile cache (`slack:user:*`) and display-name mention reverse index (`slack:user-by-name:*`) were global across installations. In multi-workspace deployments, one tenant's cached profile bled into another, and mention resolution could pick a same-named user from a different workspace. Both are now prefixed with the current installation ID (enterprise ID for org-wide installs — correct on Grid, where users are org-global). Single-workspace keys are unchanged; scoped entries repopulate on first lookup. ### `team_id` on org-token API calls Org-wide tokens span every workspace in the org, so workspace-scoped Web API methods (`conversations.list`, `usergroups.list`, …) require an explicit `team_id`. `withToken` now injects the event's `team_id` on calls made under an org-wide install when the caller didn't set one — Slack documents always passing it as safe ("accepted, but ignored" elsewhere). When an event arrives from a shared channel hosted on an "away" workspace, its `context_team_id` is echoed back as `client_context_team_id`, scoped to calls targeting the originating channel. ### Event retry deduplication Retried deliveries (`x-slack-retry-num` header, socket `retry_num`) are dropped when the original delivery was already dispatched, using an `event_id` marker in the state adapter (24 h TTL, covering Slack's opt-in Delayed Events redeliveries). First deliveries pay no state read, and events whose first delivery never arrived are still recovered via the retry — preserving the existing missed-event recovery semantics. ### W-prefixed user IDs Grid users can have `W…` IDs anywhere `U…` appears. The outgoing bare-mention scanner only recognized `@U…` as a raw user ID; it now accepts both. This was the only `^U` assumption in the package — incoming mention parsing was already prefix-agnostic. --------- 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> |
||
|
|
bb7cd1241d |
feat(slack): expose author email on incoming messages (#716)
Slack counterpart to #711: populates `message.author.email` on normalized incoming Slack messages. Unlike Teams, no new lookup was needed — `parseSlackMessage` already resolves the sender via the state-cached `users.info` call (`lookupUser`), and the cached profile already carried `email`. This change threads that value onto the author, so there is no additional API call per message. The email is only present when the app has the `users:read.email` scope; otherwise (or when the lookup is skipped, e.g. webhook posts with a `username`) the field stays `undefined`. Also documents the optional scope in the adapter README and the docs site manifest section. The core `Author.email` field and serialization already landed in #711, so this is a Slack-only changeset. Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
26c052258c |
feat(discord): add channel response allowlist (#715)
## Summary Adds an opt-in `respondToChannelIds` Discord adapter option. Non-bot messages in configured parent channels and their child threads are routed through mention handlers without requiring an @mention; top-level messages keep the adapter's existing automatic thread creation, and forwarded Gateway packets preserve the parent channel for thread replies. I understand this might be something you want to keep out but I find it very useful for my own "Hermes-like" agent :) ## Test plan - `pnpm --filter @chat-adapter/discord test` - `pnpm --filter @chat-adapter/discord typecheck` - `pnpm check` - `pnpm konsistent` - `pnpm typecheck` - `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> Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
b605cf63a2 |
fix(discord): Suppress link previews in markdown (#726)
## Summary Fixes #692 When bracketed `<link>` or `[text](<link>)` notations are used in Discord markdown, embedded link previews should be suppressed. We have to identify this notation in the markdown parser and decorate the resulting AST so the `[text](<link>)` notation can be handled as well. Otherwise, this information gets lost along the way. ## Test plan ``` await event.channel.post({ markdown: '<https://google.com>'}); ``` <img width="172" height="27" alt="image" src="https://github.com/user-attachments/assets/386ea82d-d00c-4ebd-a29b-b2cd495e973e" /> ``` await event.channel.post({ markdown: '[Google](<https://google.com>)'}); ``` <img width="199" height="29" alt="image" src="https://github.com/user-attachments/assets/0faea900-1742-482e-b477-0b5e5ac87316" /> ## 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) |
||
|
|
80def3ab17 |
feat: add author.isSystem to distinguish platform-generated messages (#707)
Closes #653 Chat SDK's normalized author only distinguished the current bot from other bots (`isBot`/`isMe`), so Slack system notifications authored by the reserved `USLACK` user — which carry no `bot_id` and no system subtype — were dispatched to handlers as if human-authored. Consumers had to hard-code `message.author.userId === "USLACK"`, leaking Slack-specific identifiers into adapter-independent code. This adds an optional `isSystem?: boolean` to the normalized `Author` type, documented so that an absent value means `false`. Keeping it optional avoids breaking existing custom adapters and serialized messages, as proposed in the issue. The Slack adapter now sets it in both parse paths (`parseSlackMessage` and the sync `parseMessage` path) via a `SLACK_SYSTEM_USER_ID` constant, so applications can write: ```ts bot.onNewMention(async (thread, message) => { if (message.author.isSystem) { return; } await generateAssistantResponse(thread, message); }); ``` Other adapters can adopt the same field when their platforms expose equivalent system-generated messages. Also included: - Regression tests covering the issue's exact case (`USLACK` DM, no `bot_id`, no subtype) across all three layers: async parse, sync `parseMessage`, and end-to-end `handleWebhook` dispatch — plus the negative case for human authors. - A `USLACK` webhook fixture in `sample-messages.md`. - `isSystem` documented in the Author type table on the Message API docs page. - Changesets for `chat` and `@chat-adapter/slack`. --------- Co-authored-by: mdnanocom <arnaud@massive-dynamic.ai> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: dancer <josh@afterima.ge> |
||
|
|
0701679e40 |
fix: Enhance mention regex and sleep method with abort support (#706)
Refactor mention regex handling and sleep method to support abort signal. a new `RegExp` is compiled on every call to `isBotMentioned`, which is invoked for every incoming message. Since `username` is stable after initialization, this regex can be compiled once (e.g. cached in a private field and updated when `_userName` changes) and reused across calls. ## Checklist - [x] All commits are signed and verified - [x] All commits are signed off for the DCO (`git commit -s`) - [x] `pnpm validate` passes - [ ] Changeset added (or N/A — see [CONTRIBUTING.md](./CONTRIBUTING.md)) - [ ] Documentation updated (or N/A) --------- Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
270b1c2592 |
fix(gchat): accept endpointUrl as a direct-webhook JWT audience (#518)
## Summary When a Google Chat app's connection setting **Authentication audience** is set to **HTTP endpoint URL** (Google's recommended option for HTTP-hosted apps that aren't behind Cloud Run IAM, see [Verify requests from Google Chat][1]), the bearer token Google sends is an OIDC ID token whose `aud` is the endpoint URL — not the GCP project number. The adapter previously only verified against `googleChatProjectNumber`, so URL-audience tokens always failed with `401 Unauthorized` and direct webhooks silently broke for any app configured this way. This change makes the adapter verify direct-webhook JWTs against `googleChatProjectNumber` and/or `endpointUrl`, accepting either when both are configured (handy for multi-env setups that mix the two modes). The constructor's fail-closed check accepts an explicit `endpointUrl` as a valid direct-webhook verifier alongside `googleChatProjectNumber`, `pubsubAudience`, and `disableSignatureVerification`. [1]: https://developers.google.com/workspace/chat/verify-requests-from-chat ## Behavior | Config | Direct-webhook `aud` accepted | | ----------------------------------------------------------- | --------------------------------- | | `googleChatProjectNumber` only (current behavior) | project number | | `endpointUrl` only (**new**) | endpoint URL | | Both `googleChatProjectNumber` and `endpointUrl` (**new**) | either | | Neither, no `pubsubAudience`, no `disableSignatureVerification` | constructor throws (unchanged) | ### Security note Auto-detected endpoint URLs (the value `handleWebhook` falls back to from the incoming `request.url` when `endpointUrl` is not configured) are intentionally **not** promoted to verifier status. Treating an auto-detected URL as a valid audience would let any caller bypass verification by hitting the bot at a URL of their choice. A new `endpointUrlIsAudience` flag captures whether the caller explicitly configured `endpointUrl`, and only that case enables URL-based verification. A regression test guards this. ## Implementation - `verifyBearerToken` accepts `string | string[]` (`OAuth2Client.verifyIdToken` already supports both). - `handleWebhook` builds `directAudiences = [projectNumber, explicitEndpointUrl].filter(Boolean)` and passes a single string when only one verifier is configured (to preserve the prior call shape) or an array when both are. - Docs updated in `apps/docs/content/adapters/official/google-chat.mdx`: describe both authentication-audience modes and document `endpointUrl` as an accepted verifier. - Changeset: `@chat-adapter/gchat: patch`. ## Test plan - [x] `pnpm --filter @chat-adapter/gchat test` — 250/250 pass (5 new tests). - [x] `pnpm typecheck` — 33/33 tasks pass. - [x] `pnpm check` (Ultracite/Biome) clean. - [x] `pnpm konsistent` clean. - [x] Validated end-to-end against a real Google Chat app configured with "HTTP endpoint URL" as authentication audience: webhooks that previously 401'd now verify and process correctly. --------- Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
d88789c982 |
fix(state-pg): setIfNotExists TTL expiry (#636)
## Summary Fixes the Postgres state adapter so `setIfNotExists()` can claim a cache key whose existing row has expired. Previously the method used `ON CONFLICT DO NOTHING`, so an expired row in `chat_state_cache` still blocked acquisition until a separate cleanup deleted it. That diverged from the memory adapter behavior and from the expected lease semantics for cache-backed coordination. The new query keeps active rows protected, but replaces rows whose `expires_at` is in the past. ## Validation - `pnpm --filter @chat-adapter/state-pg test` - `pnpm --filter @chat-adapter/state-pg typecheck` - `pnpm exec ultracite check packages/state-pg/src/index.ts packages/state-pg/src/index.test.ts` --------- Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
46681f50cb |
fix(teams): hydrate incoming author email (#711)
## Summary Adds optional email to normalized message authors and preserves it through message serialization. For incoming Teams messages, resolves the sender with the activity's Entra object ID before dispatch, falling back to the cached ID when the activity omits it. The lookup reuses the existing `mail ?? userPrincipalName` mapping from #708, and missing permissions or Graph failures leave email undefined without blocking message delivery. This deliberately revisits the author-profile boundary discussed in #239: the core field is optional, and Teams populates it only when Microsoft Graph can resolve the sender. ## Test plan - [x] `pnpm --filter chat exec vitest run src/message.test.ts` - [x] `pnpm --filter @chat-adapter/teams exec vitest run src/index.test.ts` - [x] Chat and Teams package typechecks and builds - [x] `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> Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
93a58af563 |
fix(teams): preserve native streaming with placeholders (#709)
## Summary Preserves Teams native DM streaming when `fallbackStreamingPlaceholderText` is explicitly configured. Direct messages show the text through the Teams SDK native informative status before streaming the answer, group chats use the core post-and-edit fallback, `null` disables progress, and omitted configuration keeps the existing native-DM/buffered-group behavior. ## Test plan - [x] `pnpm --filter @chat-adapter/teams exec vitest run src/index.test.ts` - [x] `pnpm --filter chat exec vitest run src/chat.test.ts src/thread.test.ts` - [x] Teams and Chat package typechecks and builds - [x] `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> |
||
|
|
3895ab3fa9 |
fix(teams): fall back to user principal name for email (#708)
## Summary Use Microsoft Graph's `userPrincipalName` as the Teams user's email when `mail` is missing, while preserving `mail` precedence when both fields are present. ## Test plan - [x] `pnpm --filter @chat-adapter/teams test` - [x] `pnpm --filter @chat-adapter/teams typecheck` - [x] `pnpm --filter @chat-adapter/teams build` - [x] `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 (N/A — no public API change) Signed-off-by: onmax <maximogarciamtnez@gmail.com> |
||
|
|
f84b5911e7 |
chore(release): version packages (#695)
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.34.0 ### Minor Changes - |
||
|
|
5c926f1987 |
fix(chat): preserve markdown whitespace in plain text (#604)
## Problem
When I mention a Chat SDK-powered bot on GitHub like this:
```
@bot
hi there!
```
The mention doesn't trigger any handling, because the `mdastToString`
helper replaces all newlines with an empty string, so the logs look like
this:
```
[chat-sdk] Checking message patterns { patternCount: 0, patterns: [], messageText: '@bothi there' }
[chat-sdk] No handlers matched message {
threadId: 'github:RSO/chat:issue:29',
text: '@bothi there'
}
```
## Summary
- Preserve structural markdown whitespace when extracting normalized
plain text from mdast.
- Add regression coverage for newline-separated bot mentions in core
markdown extraction, GitHub issue/review comments, and Chat mention
routing.
- Add a patch changeset for the behavior fix.
## Testing
- `pnpm check`
- `pnpm knip`
- `pnpm test:workspace`
- `pnpm --filter chat test`
- `pnpm --filter @chat-adapter/github test`
- `pnpm --filter chat typecheck`
- `pnpm --filter @chat-adapter/github typecheck`
## Notes
- `pnpm validate` was attempted and reached the full Turbo test graph,
but failed on `packages/integration-tests/src/replay-discord.test.ts`
(`should skip bot's own messages in subscribed threads`). Rerunning
`pnpm --filter @chat-adapter/integration-tests test --
src/replay-discord.test.ts` passed, so this appears unrelated to the
markdown whitespace change.
- `.github/CONTRIBUTING.md` requires signed commits. This environment
has no `gpg` binary and no SSH signing identities loaded, so the commit
in this PR is currently unsigned and may need to be re-signed before
merge.
---------
Signed-off-by: dancer <josh@afterima.ge>
Co-authored-by: dancer <josh@afterima.ge>
|
||
|
|
6714efc3a1 |
feat: support AI SDK v7 (ai@7) as a peer dependency (#691)
Closes #690 ## What Widens the AI SDK peer dependency ranges so the Chat SDK installs cleanly next to `ai@7`: - `chat`: `ai@^6.0.182 || ^7.0.0` - `@chat-adapter/web`: `ai@^6 || ^7`, `@ai-sdk/react@^3 || ^4`, `@ai-sdk/svelte@^4 || ^5`, `@ai-sdk/vue@^3 || ^4` This also unbreaks `create-chat-sdk` scaffolds, which install `ai@latest` (now v7) next to `chat` and currently hit a peer conflict out of the box. ## The one real v6 → v7 break In v7, `tool()` with an `execute` function returns `ExecutableTool<Tool<...>>` — an internal type from `@ai-sdk/provider-utils` that `ai` does not re-export. The `chat/ai` tool factories relied on inference, so declaration emit failed with TS2742 (17 errors). The factories now declare explicit `Tool<Input, Output>` return types, which is exactly the shape the previously published `.d.ts` already had — the public type surface is unchanged, and the emitted declarations only reference types from `ai` (portable for consumers on either major). Everything else checked out compatible: - v7 stream parts keep `text-delta` / `finish-step` shapes, so `fromFullStream` duck-typing works unchanged; `fullStream` remains as a deprecated alias - tool-level `needsApproval` is deprecated in v7 but still typed and honored - `createUIMessageStream`, `createUIMessageStreamResponse`, `isTextUIPart`, `UIMessage`, `UIMessageStreamWriter`, `ChatInit`, `DefaultChatTransport` all still exported — `@chat-adapter/web` needed zero source changes ## Other changes - devDependencies move to v7 so the workspace develops/tests against the latest major - `examples/nextjs-chat` and `examples/nuxt-chat` move to `ai@^7` (required — mixing majors across the workspace fails typecheck, since `chat`'s d.ts resolves `ai` types from its own devDependency) - Test-only: the `ToolExecutionOptions` stub type is now derived from `Tool["execute"]` because v7 made the generic parameter required - Changeset included (minor for `chat` and `@chat-adapter/web`) ## Verification The same source was verified against **both majors** (`ai@6.0.182` and `ai@7.0.17`): `tsc --noEmit` and the full test suites (`chat`: 1035 tests, `@chat-adapter/web`: 21 tests) pass on each. `pnpm validate` (knip + check + typecheck + test + build, including both examples) is green on v7. Note for adopters: `ai@7` itself requires Node.js ≥ 22 and is ESM-only; `chat` keeps `engines.node >= 20` since `ai` is an optional peer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: chentsulin <chentsulin@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: dancer <josh@afterima.ge> |
||
|
|
2338a66544 |
feat(whatsapp): add sendTemplate for pre-approved template messages (#588)
Closes #585 ## Summary Adds `sendTemplate()` to the WhatsApp adapter for sending pre-approved [Message Templates](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-message-templates) — the only message type the Cloud API accepts outside the 24-hour customer service window, and therefore required for business-initiated conversations (notifications, reminders, re-engagement). The inbound half already existed (`handleButtonResponse` dispatches template quick-reply taps to `onAction` handlers); this completes the outbound side. Notably, the package's `AGENTS.md` already documented `sendTemplate` and `WhatsAppTemplateMessage` as part of the public surface — this PR implements exactly that documented API. ## Changes - **`sendTemplate(threadId, template)`** on `WhatsAppAdapter` — posts a `type: "template"` payload through the existing `graphApiRequest` path and returns a `RawMessage`, mirroring `sendInteractiveMessage`/`sendSingleTextMessage` - **Types**: `WhatsAppTemplateMessage`, `WhatsAppTemplateComponent`, `WhatsAppTemplateParameter`, `WhatsAppTemplateButtonParameter` in `types.ts`, modeled on the Cloud API template object (header/body/button components; text, currency, date_time, and media parameters), re-exported from the package entry point - Templates are kept out of the `PostableMessage`/mdast pipeline — they're sent by name + variable components, not free-form markdown, and the adapter intentionally does not auto-substitute templates for outbound text posts (per AGENTS.md) - `openDM()` JSDoc now links to `sendTemplate` for the business-initiated path ## Docs - New "Template messages" section in the adapter README and `apps/docs/content/adapters/official/whatsapp.mdx` with a usage example - Added "Template messages" row to the README feature table - Corrected the feature-matrix frontmatter labels: `cardFormat` "WhatsApp templates" → "Interactive messages" and `fields` "Template variables" → "Formatted text" — cards render as Cloud API interactive messages (as the page body already states), and the old labels would now wrongly imply cards go through the new template API ## Usage ```typescript const threadId = await adapter.openDM("15551234567"); await adapter.sendTemplate(threadId, { name: "appointment_reminder", language: "en", components: [ { type: "body", parameters: [{ type: "text", text: "Tomorrow at 2pm" }], }, ], }); ``` ## Testing - 5 new tests in `index.test.ts` following the existing `fetch`-spy pattern: payload shape (name/language/`to`), component pass-through (body + URL button), empty-components omission, missing-message-ID error, and invalid thread ID rejection - `pnpm validate` (knip, check, typecheck, test, build) passes — 117/117 adapter tests green Includes a `minor` changeset for `@chat-adapter/whatsapp`. --------- Signed-off-by: dancer <josh@afterima.ge> Co-authored-by: dancer <josh@afterima.ge> |
||
|
|
57fa85ca54 |
test(telegram): raise rich-truncate test timeout to 15s (#703)
## summary the `rich markdown messages truncate at the rich message limit` test runs ~5.5s in CI (coverage + shared runner) and intermittently exceeds the 5s default, failing unrelated PRs. raise its timeout to 15s ## test plan - `pnpm --filter @chat-adapter/telegram test` Signed-off-by: dancer <josh@afterima.ge> |
||
|
|
0f743c9b33 |
feat(slack): support native Slack agents (#698)
Builds on the Agent messaging experience support from #684 with a declarative config layer for building Slack agents, plus hardening for native streaming. Everything is configured on `createSlackAdapter()` — no per-event wiring required. ## Slack adapter (`@chat-adapter/slack`) ### `suggestedPrompts` Static payload or per-thread resolver, applied automatically when an assistant/agent thread opens: - `assistant_thread_started` (legacy `assistant_view`), with the thread's `thread_ts` - Messages-tab `app_home_opened` (with `agentView` enabled), without `thread_ts` so prompts pin atop the agent conversation The resolver receives the thread context (`channelId`, `userId`, legacy `threadTs`/`teamId`/`enterpriseId`, and normalized active-view `entities` under `agentView`); returning `null`/`undefined` skips the thread. Prompts beyond Slack's 4-prompt limit are dropped with a warning. Resolver/API failures are logged, never a webhook 500. Applied via `waitUntil` inside the request scope so multi-workspace token context propagates. ### `loadingMessages` Default rotating status strings for the assistant thinking indicator, used by `startTyping` and `setAssistantStatus` when no explicit status/messages are passed. ### `nativeStreaming` + automatic post-and-edit fallback - New `nativeStreaming` config (default `true`). Set `false` on Slack flavours without the `chat.startStream` family (e.g., GovSlack) to always stream via post-and-edit. - If the workspace rejects the **first** native streaming call, `stream()` falls back to throttled post-and-edit mid-stream instead of failing the reply; already-consumed text is preserved (it lives in the renderer). Permanent platform errors (`unknown_method`, `method_deprecated`, `feature_not_enabled`) latch native streaming off for subsequent streams on the adapter instance; transient errors don't latch. - Structured chunks (`task_update`/`plan_update`) are skipped in fallback mode; failures after native content has rendered still propagate (mixing surfaces would duplicate output). - Also updates the stale streaming description in the package AGENTS.md (the adapter now streams via `chat.startStream`/`appendStream`/`stopStream`, not `chat.update`). ### `feedbackButtons` Appends Slack's native thumbs up/down (a `context_actions` block with a `feedback_buttons` element) to every streamed reply on `chat.stopStream`, after any `StreamingPlan` `endWith` blocks. Pass `true` for defaults or an options object (`actionId`, labels, values). Clicks dispatch through the regular `block_actions` flow to `bot.onAction` with a positive/negative value — no new plumbing. Exports `buildFeedbackButtonsBlock(options?)` for attaching the same block to non-streamed messages. New exported types: `SlackFeedbackButtonsOptions`, `SlackSuggestedPrompt`, `SlackSuggestedPrompts`, `SlackSuggestedPromptsContext`, `SlackSuggestedPromptsOptions`. ## Docs - Configuration table rows for `agentView`, `suggestedPrompts`, `loadingMessages`, `nativeStreaming`, `feedbackButtons`. - New "Native streaming" and "Feedback buttons" sections plus declarative suggested-prompts examples. - All agent content grouped under a new **Advanced → Agents** subsection (Agent messaging experience → Assistants API → Native streaming → Feedback buttons). Heading titles unchanged, so existing anchors keep resolving. - TypeTable descriptions rewritten as plain text (they don't render markdown). ## Example app (`examples/nextjs-chat`) - `SLACK_AGENT_OPTIONS` shared across both Slack adapter branches: active-view-aware `suggestedPrompts` resolver, `loadingMessages`, `feedbackButtons` with an `ai_feedback` acknowledgment handler. - Env toggles: `SLACK_AGENT_VIEW` (agent_view mode) and `SLACK_NATIVE_STREAMING` (compare native vs post-and-edit). - Commented `agent_view` blocks in `slack-manifest.yml` (feature block, `assistant:write` scope, agent events) with a note that the switch is irreversible. - AI flows call `startTyping()` without an explicit status so configured loading messages rotate. ## Test plan - `pnpm validate` and `pnpm konsistent` pass. - 26 new unit tests: suggested prompts (static/resolver/agent_view/truncation/error paths), loading message defaults, native streaming fallback (opt-out, mid-stream fallback, permanent-error latching, transient non-latching, propagation after native render, structured-chunk skipping), feedback buttons (block shape, custom options, ordering after `endWith`, webhook round-trip of a click). - Each adapter commit was built and verified independently (typecheck + full suite green at every step) for bisectability. - Verified manually against a live `agent_view` workspace: prompts pinned on thread open, loading messages rotating in the thinking indicator, native token-by-token streaming in DMs and channel threads, post-and-edit fallback via the opt-out flag, and feedback clicks dispatching to `onAction`. ## Notes - One changeset covers the three adapter features (`minor` for `@chat-adapter/slack`). - Known follow-up (not in this PR): under `agentView`, a subscribed conversation-scoped DM thread (the #684 openDM bridge) routes DM messages to a thread without `thread_ts`, which silently pins DMs to post-and-edit. Worth deciding whether the bridge should keep per-message threading for replies or log loudly when it redirects. ## Screenshots | Suggested Prompts | Feedback Buttons | | --- | --- | | <img width="647" height="347" alt="CleanShot 2026-07-13 at 13 26 27" src="https://github.com/user-attachments/assets/4c89932b-ed19-4b4a-83ae-d3d022d0c120" /> | <img width="825" height="276" alt="CleanShot 2026-07-13 at 13 28 23" src="https://github.com/user-attachments/assets/6bdf6979-5209-4bed-b0bc-dfbee7165455" /> | --------- Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
8bd8a57518 |
feat(whatsapp): send outbound files and attachments via Cloud API (#537)
Implement media upload, MIME mapping, caption fallbacks, and card+file sequencing. ## Summary The WhatsApp adapter previously ignored `files` and `attachments` on outbound `post()` calls (only text and interactive cards were sent). This PR implements full outbound media support via the [WhatsApp Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api/reference/media): 1. **Binary upload** — `POST /{phoneNumberId}/media` → `media_id` → typed media message 2. **Link passthrough** — HTTPS `Attachment.url` sent directly (no upload) 3. **Multi-file** — one WhatsApp message per file/attachment, sent sequentially 4. **Captions** — markdown or card fallback text on the first media message when supported 5. **Card + files** — media first, then interactive buttons (when applicable) **Packages:** `@chat-adapter/whatsapp` (minor) --- ## Supported inputs | Input | Description | |-------|-------------| | `files: FileUpload[]` | Binary buffers/blobs with `filename` and optional `mimeType`. Always uploaded via `/media`. | | `attachments: Attachment[]` | Typed media (`image` \| `file` \| `video` \| `audio`). Binary via `data` / `fetchData`, or HTTPS URL-only via `url`. | Both can be combined on `{ markdown }`, `{ raw }`, `{ ast }`, or `{ card }` postables. `files` are processed first, then `attachments`. ### Examples ```typescript // PDF with caption await thread.post({ markdown: "Here's the report", files: [{ data: pdfBuffer, filename: "report.pdf", mimeType: "application/pdf" }], }); // Multiple files (N sequential messages) await thread.post({ markdown: "Two files attached", files: [ { data: buf1, filename: "a.pdf", mimeType: "application/pdf" }, { data: buf2, filename: "b.png", mimeType: "image/png" }, ], }); // Card with buttons + image file await thread.post({ card: approvalCard, files: [{ data: proofBuffer, filename: "proof.png", mimeType: "image/png" }], }); // Files only (no text) await thread.post({ markdown: "", files: [{ data: buffer, filename: "data.xlsx" }], }); ``` --- ## Behavior reference ### Message flow (with media) ``` postMessage() ├─ files or attachments present? │ YES → postMessageWithMedia() │ ├─ Resolve text (card fallback OR markdown/raw/ast) │ ├─ Caption strategy (see below) │ ├─ For each file/attachment: upload (if binary) → sendMediaMessage() │ └─ Card present? │ ├─ interactive buttons → sendInteractiveMessage() │ └─ text-only card fallback → sendTextMessage() (if caption didn't already send text) │ └─ NO → existing text / card-only path (unchanged) ``` ### Multi-file WhatsApp allows **one media object per API message**. Multiple `files` or `attachments` in a single `post()` produce **N sequential messages**. The returned `RawMessage` is the **last** one sent (same convention as long-text chunking). | File index | Caption | |------------|---------| | First | Markdown / card fallback text (when caption rules allow) | | 2…N | No caption | ### Caption placement | Condition | Behavior | |-----------|----------| | Text ≤ 1024 chars, first media is not `audio`, media supports captions | Text sent as **caption** on first media message | | Text > 1024 chars | **Separate text message first**, then media with no captions | | First media is `audio` | **Separate text message first** (audio does not support captions), then audio | | No text (`markdown: ""`, files only) | Media only, no caption | ### MIME type → WhatsApp message type | MIME | WhatsApp `type` | |------|-----------------| | `image/jpeg`, `image/png` | `image` | | Other `image/*` (e.g. GIF, WebP, SVG) | `document` | | `video/mp4`, `video/3gpp` | `video` | | `audio/*` | `audio` | | Everything else (PDF, XLSX, etc.) | `document` | For `Attachment` without `mimeType`, the adapter uses `attachment.type` (`image` → image, `file` → document, etc.), then applies MIME rules when `mimeType` is set. ### Size limits (pre-flight) Throws `ValidationError` when binary size is known (before upload): | Type | Limit | |------|-------| | `image` | 5 MB | | `audio` | 16 MB | | `video` | 16 MB | | `document` | 100 MB | URL-only attachments skip size validation unless `attachment.size` is provided. ### Card + files When both a **card** and **files/attachments** are present: 1. **Media message(s)** first — caption uses `cardToFallbackText(card)` on the first media item 2. **Card message** second: - Valid reply buttons (1–3) → interactive button message - Otherwise → text fallback message (skipped if text was already sent as a leading message) ### Card image vs `files` (important) | How image is provided | Result | |-----------------------|--------| | `<Image>` child or `card.imageUrl` only (no `files`) | **No real image media.** Card becomes interactive text or text fallback; image URL may appear as plain text in fallback. | | `files` / `attachments` + card with buttons | **Real image message** + separate interactive button message | To send a photo with buttons, pass the image via `files` or `attachments`, not only as a card image child. ### Link passthrough - `Attachment` with **only** `url` (no `data` / `fetchData`) → `{ link: url }` in the media payload - URL **must** be `https://` - No `/media` upload call ### Binary resolution | Source | Path | |--------|------| | `FileUpload.data` | `toBuffer()` → `uploadMedia()` → `{ id }` | | `Attachment.data` / `fetchData` | Same | | `Attachment.url` only | `{ link }` passthrough | --- ## Out of scope (follow-ups) - Stickers (WebP encoding requirements) - Voice notes (`voice` vs `audio` distinction) - Media ID caching across posts (30-day expiry) - Interactive message **image headers** (card-embedded images without `files`) - Replay integration test mock extensions for `/media` - Edit/replace flows that include files --------- Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
2531a4227e |
fix username regexp (#621)
## Summary <!-- What does this PR do? --> Fix `detectMention` falsely matching `@bot` when `@bot-dev` is mentioned. `\b` (word boundary) matches between a word character and a hyphen, so `/@bot\b/` incorrectly matches `@bot-dev`. Replaced with `(?![\w-])` to exclude hyphens. ## Test plan <!-- How did you verify the changes? --> ## Checklist - [x] All commits are signed and verified - [x] `pnpm validate` passes - [x] Changeset added (or N/A — see [CONTRIBUTING.md](./CONTRIBUTING.md)) - [x] Documentation updated (or N/A) --------- Signed-off-by: sivchari <shibuuuu5@gmail.com> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
4bca64f058 |
feat(x): support image uploads on posts and DMs (#700)
## summary
the X adapter previously rejected every attachment (`File uploads are
not supported by the X adapter yet`). this adds image upload support:
images passed as `files` or `attachments` are uploaded through X's v2
chunked media endpoints and attached to the resulting post or DM
- uploads via X's current path-based flow: `POST
/2/media/upload/initialize` (JSON) then `/{id}/append` (multipart) then
`/{id}/finalize`, reusing the adapter's managed OAuth token
- attaches `media_ids` on `POST /2/tweets` for posts, and `attachments`
for DMs
- supports png, jpeg, and webp, up to 4 per post, with or without text
- requires the `media.write` OAuth 2.0 scope on the user token
### before / after
- before: posting a message with `files`/`attachments` throws a
`ValidationError`
- after: images upload and attach, and a post can be media-only or media
plus text
<details>
<summary>usage</summary>
```typescript
await thread.post({
markdown: "France lead the title race",
files: [{ data: pngBuffer, filename: "odds.png", mimeType: "image/png" }],
});
```
</details>
## test plan
- added unit tests covering the initialize JSON body, the multipart
append path, finalize, `media_ids` on the tweet, DM `attachments`,
media-only posts, MIME inference from filename, the over-limit
rejection, and unsupported-type rejection
- verified live against the X API end to end: uploaded an image and
posted then deleted it through the adapter (the initial command-param
implementation 400'd against the live API, which is what surfaced the
path-based endpoints as required)
- `pnpm --filter @chat-adapter/x build`, tests, `pnpm exec biome check`,
and `pnpm konsistent` all pass
---------
Signed-off-by: dancer <josh@afterima.ge>
|
||
|
|
8d7ccdb11b |
feat(telegram): support multiple file and attachment uploads (#605)
## Summary Adds Telegram media group support for posting multiple files and compatible typed attachments. Multiple `files` are sent as document media groups, while `attachments` preserve image, video, audio, or file media types and use Telegram’s native `sendMediaGroup`. --------- Signed-off-by: dancer <josh@afterima.ge> Co-authored-by: dancer <josh@afterima.ge> |
||
|
|
0fdb902980 |
feat(discord): Components support (#678)
## Summary #### What Adds opt-in support for Discord [Components](https://docs.discord.com/developers/components/reference). #### Why Discord Components allows developers more control over the layout of bot messages by treating text, images, files, and buttons as flexible components. Instead of the rigid text-above-embeds layout, elements can be arranged in any order or column. An extreme example of what's possible with Components: <img width="728" height="1117" alt="image" src="https://github.com/user-attachments/assets/fde6ab9c-f635-45fb-b64f-9ddf4c26580f" /> #### How Embeds remain the default behavior. The Discord adapter now supports a `componentsV2` flag to When enabled, card messages render with Discord Components v2 containers, sections, text displays, media galleries, separators, buttons, and string selects, and include the `IS_COMPONENTS_V2` message flag. ## Test plan Create a Chat with the Discord adapter. Set `contentFormat: DiscordContentFormat.ComponentsV2` and create a post that uses sections, markdown, buttons, etc. Note that 1. All elements should render correctly. 2. Individual sections can contain their own actions. 3. Markdown formatting gets rendered correctly. ``` import { Actions, Button, Card, CardText, Image, LinkButton, Section } from "chat"; import { createDiscordAdapter } from "@chat-adapter/discord"; const discord = createDiscordAdapter({ contentFormat: DiscordContentFormat.ComponentsV2, }); await thread.post( <Card title="Deployment ready" subtitle="Production build completed"> <Section> <CardText> **Version 2.4.0** is ready to promote. Review the release notes, then choose an action below. </CardText> <Image url="https://example.com/deploy-preview.png" alt="Preview" /> </Section> <Actions> <Button id="promote" style="primary"> Promote </Button> <Button id="rollback" style="danger"> Roll back </Button> <LinkButton url="https://example.com/deployments/123"> View deployment </LinkButton> </Actions> </Card> ); ``` --------- Signed-off-by: dancer <josh@afterima.ge> Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> Co-authored-by: dancer <josh@afterima.ge> |
||
|
|
6c2a3918d5 |
feat(discord): rename thread channels (#693)
## Summary Add `DiscordAdapter#setThreadTitle()` to rename native Discord thread channels and document the required **Manage Threads** permission. Signed-off-by: onmax <maximogarciamtnez@gmail.com> Signed-off-by: dancer <josh@afterima.ge> Co-authored-by: dancer <josh@afterima.ge> |
||
|
|
5341f909a4 |
feat(discord): ignore @everyone/@here pings unless respondToGlobalMentions is set (#701)
## Summary Fixes #699. In gateway mode, the bot responded to `@everyone`/`@here` announcements. The legacy gateway listener used discord.js `message.mentions.has(botId)`, which counts global pings as a mention of the bot by default. Global pings are now ignored on both gateway paths by default, and a new `respondToGlobalMentions` config option (default `false`, per the issue's suggestion) lets users opt back in: - **Legacy gateway mode**: mention detection passes `{ ignoreEveryone: true }` to `mentions.has()`; `@everyone`/`@here` only count via `message.mentions.everyone` when the option is enabled. Direct mentions, `mentionRoleIds` role mentions, and replies to the bot are unaffected. - **Forwarded gateway mode**: added the `mention_everyone` field to `DiscordGatewayMessageData` and wired it into the same gate (this path previously ignored global pings silently, with no way to opt in). ## Changes - `respondToGlobalMentions?: boolean` on `DiscordAdapterConfig` (default `false`) - Mention detection updated in both `setupLegacyGatewayHandlers` and `handleForwardedMessage` - 5 new tests covering both transports: global pings ignored by default, honored when opted in, and direct mentions still detected while global pings are ignored - Docs: config tables in `apps/docs/content/adapters/official/discord.mdx` and the adapter README - Changeset (minor, `@chat-adapter/discord`) ## Testing - `pnpm validate` passes (knip + check + typecheck + test + build) - All 249 `@chat-adapter/discord` tests pass, including the 5 new ones 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Faraz Patankar <farazpatankar@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4717a38407 |
feat(slack): support data table and data visualization blocks (#696)
Adds support for Slack's [data table](https://docs.slack.dev/reference/block-kit/blocks/data-table-block) and [data visualization](https://docs.slack.dev/reference/block-kit/blocks/data-visualization-block) Block Kit blocks. - **`chat`**: new cross-platform `ChartElement` + `Chart()` builder (JSX supported) mirroring Slack's model — pie `segments`, or bar/area/line `series` against shared `categories`. `Table()` gains optional `caption` and `pageSize`. Charts degrade to a text table on other platforms via the shared card fallback (`chartElementToFallbackText`). - **`@chat-adapter/slack`**: card tables now render as paginated, sortable `data_table` blocks by default (header-only tables keep the plain `table` block; oversized tables still fall back to ASCII). Charts render as `data_visualization` blocks; charts violating Slack constraints — including the undocumented **max 2 charts per message** — fall back to a text rendering instead of an API rejection. Same treatment in the `@chat-adapter/slack/blocks` subpath. - **`postMessage`** now surfaces Slack's per-block validation messages on `invalid_blocks` errors (this is how the 2-chart limit was found). - Example app gets a **Show Charts** button and table pagination on **Show Table**; docs, feature matrices, and changeset updated. Verified live against Slack: data table pagination/sorting and both chart types render natively. <table> <tr> <th>Data Table</th> <th>Data Charts</th> </tr> <tr> <td><img width="979" height="896" alt="CleanShot 2026-07-12 at 23 28 32" src="https://github.com/user-attachments/assets/3307bd90-9322-452f-86fb-07d46446822d" /></td> <td><img width="955" height="879" alt="CleanShot 2026-07-12 at 23 29 02" src="https://github.com/user-attachments/assets/ddb31a1b-e3fd-457c-a2e6-bde4934afebe" /></td> </tr> </table> --------- Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
1721fa01e7 |
feat(slack): add Slack Agent messaging experience (agent_view) support (#684)
## Summary
Add support for Slack's Agent messaging experience (`agent_view`), the
2026 replacement for `assistant_view`.
## Core (`chat`)
- New `onAppContextChanged` event carrying the active-view context as a
normalized `AppContextEntity[]` (`channel` / `canvas` / `list` /
`message` / `unknown`) describing what the user is currently viewing.
- `AppHomeOpenedEvent` now carries:
- the same folded active-view context as optional `entities`
- the opened `tab` (`"home"` / `"messages"`), so handlers can
distinguish a Home-tab open from the DM-open signal under `agent_view`
## Slack adapter (`@chat-adapter/slack`)
- **`agentView` config flag.** Under `agent_view`:
- `app_home_opened` is the DM-open signal and fires regardless of tab
(branch on `event.tab` if you also publish a Home view)
- DM messages are threaded per Slack's new model — each user message is
a thread root (`thread_ts ?? ts`)
- conversation-scoped threads returned by `openDM()` keep working: when
that thread is subscribed, incoming top-level DM messages route to it,
so `onSubscribedMessage` and per-thread state behave the same as in
legacy mode
- **`app_context_changed` routing** with normalized entities. Malformed
payloads degrade gracefully: a missing `context` yields `entities: []`,
and entities with a null/malformed `value` normalize to `kind:
"unknown"` — never a webhook 500.
- **`getAppContext(message)`** helper to read the folded active-view
context off a DM message.
- **`setSuggestedPrompts`** accepts an optional thread reference
(`agent_view` lets prompts sit at the top of the agent conversation).
- **Env auth fallback now keys off auth fields**: `SLACK_BOT_TOKEN` /
`SLACK_CLIENT_ID` / `SLACK_CLIENT_SECRET` fallback is disabled only when
an auth-related field (`botToken`, `clientId`, `clientSecret`,
`installationProvider`) is passed explicitly, rather than by the
presence of any config object. This lets non-auth options compose with
env auth — e.g. `createSlackAdapter({ agentView: true })` picks up env
credentials — and matches the semantics documented in the adapter's
AGENTS.md. *(Behavior change for callers passing non-auth-only configs
while relying on env vars being ignored.)*
- Bumped `@slack/web-api` to `^7.18.0` (adds the optional `thread_ts`
typing for `setSuggestedPrompts`).
## Docs
- New "Agent messaging experience" section on the Slack adapter page
(config, manifest snippet, threading model, openDM bridge).
- "Handling active-view context" section in handling-events, plus
`tab`/`entities` rows on the app-home event table.
- Callout: under `agent_view`, bot replies are threaded per user
message, so `conversations.history` only returns the user's side of a DM
— build AI conversation history from transcripts instead of channel
history.
## Example app (`examples/nextjs-chat`)
- Plain `SLACK_BOT_TOKEN` adapter branch (previously Slack was only
wired via Vercel Connect).
- DM AI history built from transcripts instead of channel history (see
docs callout above); assistant turns persisted.
- The `dm me` trigger regex now matches mention text, which carries the
`@bot` prefix on Slack.
## Test plan
- `pnpm validate` and `pnpm konsistent` pass.
- Unit tests cover the new events, entity normalization (including
malformed payloads), `agent_view` DM threading, the openDM subscription
bridge, `tab` passthrough, `setSuggestedPrompts` thread handling, and
env-fallback behavior; an integration replay test exercises the full
webhook flow.
- Verified manually against a live `agent_view` workspace:
`onAppContextChanged` entities, folded context on `app_home_opened` and
DM messages, `tab` values for both tabs, per-message DM threading, the
openDM subscription bridge, and signed malformed-payload replays (all
return 200).
- Legacy regression pass with `agentView` off: conversation-scoped DM
threading, Home-tab-only `app_home_opened`, mention flow unchanged.
### Slack references
- Agent messaging experience:
https://docs.slack.dev/changelog/2026/06/30/agent-messages-tab/
- Active-view context:
https://docs.slack.dev/changelog/2026/07/02/app-context/
## 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
- [x] Documentation updated
---------
Signed-off-by: Damian Borowy <301205838+damianborowy-nexos@users.noreply.github.com>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
|
||
|
|
1dff4515e2 |
refactor(docs): migrate chat-sdk.dev to @vercel/geistdocs (#686)
## Summary Migrates `apps/docs` from locally-copied geistdocs runtime code to the published [`@vercel/geistdocs`](https://www.npmjs.com/package/@vercel/geistdocs) package (1.8.2), following the official [migration guide](https://preview.geistdocs.com/docs/migration). Net **−8,400 lines**. ### Package-backed now - Docs page + layouts: `createDocsPage`, `GeistdocsDocsLayout`, `GeistdocsHomeLayout` (JSON-LD + sr-only markdown hints preserved via `renderTop`) - Navbar (OSS product switcher via `navbarOssProducts`), footer, provider, search dialog, page actions (edit source, feedback, copy page, Ask AI, open-in-chat, scroll top) - `/api/search` → `createSearchRoute`, `/api/chat` → `createChatRoute` (AI SDK v6; AI Gateway default, optional `GEISTDOCS_CHAT_PROXY_URL`) - `llms.mdx` → `createDocsMarkdownRoute`, `sitemap.md` → `createSitemapMarkdownRoute` (now includes an **Adapters** section) - **New**: `/agents.md` via `createAgentsRoute`, backed by a new `agent` readiness config - `proxy.ts` → `createProxy` with explicit `markdownRoutes` for `/docs` → `llms.mdx` and `/adapters` → `adapters.mdx` (adds AI-agent UA rewrites) - CSS: `@vercel/geistdocs/styles.css` + slim local overrides (shadcn tokens for remaining `components/ui`, body tint, prose inline code, `#nd-*` tweaks); code blocks now use the geist Shiki theme - Icons/logos from `@vercel/geistdocs/assets/*`; feedback via the package action (same geistdocs.com endpoint + `siteId`) ### Kept local by design - Curated `/llms.txt` index + `/llms-full.txt` corpus — the published `AGENTS.md`/SKILL.md artifacts and integration tests reference this exact contract - The adapters section (README fetching, OG images, JSON-LD, feature matrices, `adapters.mdx` markdown route) — now rendered inside the package docs layout - RSS and OG image routes (app-owned per the migration guide) - Skipped `/.well-known/mcp.json`: no MCP servers configured, and the proxy matcher must keep excluding `.well-known` for the served agent-skills files ### Cleanup - Deleted local copies: `components/geistdocs/*` chrome, `components/ai-elements/*`, chat hooks/persistence, feedback server actions, unused shadcn primitives, geistcn logo/icon fallbacks covered by package assets - Removed 13 now-unused deps (`ai@5`, `@ai-sdk/react@2`, `dexie`, `jotai`, `cmdk`, `vaul`, `mermaid`, `nanoid`, `react-player`, `use-stick-to-bottom`, `@orama/tokenizers`, `dexie-react-hooks`, `next-themes`) - Updated `docs-llms.test.ts` proxy assertions to the `createProxy` markdown-route shape ### Behavior changes to be aware of - Code blocks use the geist Shiki theme instead of GitHub light/dark - Ask AI history is no longer persisted in IndexedDB (package owns the panel) - Adapters sidebar uses the standard geistdocs tree rendering instead of the bespoke grouped sidebar - Per-page markdown output appends the standard geistdocs footer links (`/sitemap.md`, `/llms.txt`, `/agents.md`) ## Test plan - `pnpm validate` green (knip + check + typecheck + test + build) - Smoke-tested against `next build && next start`: `/`, `/docs`, `/adapters`, `/agents.md`, `/llms.txt`, `/llms-full.txt`, `/sitemap.md`, page-level `.md` URLs for both docs and adapters, `Accept: text/markdown` negotiation, search API, JSON-LD, sr-only markdown hints, edit-source URLs (`apps/docs/content/docs/{path}`), OSS navbar, page actions - Verified compiled CSS chunks contain the home grid, Shiki palette, and geist utilities (note: stale turbopack dev caches from before this change can serve incomplete CSS — `rm -rf apps/docs/.next` fixes it) ## 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 — docs app + tests only, no package behavior change) - [x] Documentation updated (or N/A) --------- Signed-off-by: molebox <rich@vercel.com> |
||
|
|
b3123815fa |
docs: use xai/grok-4.5 across docs, guides, and examples (#687)
## summary - swap chat model strings to `xai/grok-4.5` across the docs site, the shipped guides, and the example bots, so the docs lead with the latest model - apps/docs: ai overview, ai-sdk-tools, and streaming pages, the two landing-page code samples, and the live chat demo route (`app/api/chat/route.ts`, previously `openai/gpt-4.1-mini`) - packages/chat/resources/guides: the seven guides that use a chat model (slack connect, slack + ai sdk, liveblocks, vercel blob, github code review, daily digest, ai gateway) - examples: nextjs-chat and nuxt-chat bots - deliberately left non-chat model strings as they were, since grok-4.5 cannot fill those roles: the `openai/text-embedding-3-small` embedding model and the `openai/gpt-4o-mini` reranker - in the ai gateway guide, grok-4.5 is now the primary model but the fallback list stays cross-provider (`anthropic/claude-opus-4.8`, `google/gemini-3.1-pro-preview`) so the failover example still demonstrates real cross-provider fallback - also fixed a pre-existing prose/code mismatch in that guide, the fallback prose said `claude-opus-4.7` while the code listed `4.8`, now aligned to `4.8` --------- Signed-off-by: dancer <josh@afterima.ge> |
||
|
|
b1940d2374 |
chore(release): version packages (#660)
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/github@4.33.0 ### Minor Changes - |
||
|
|
ef2542c5fd |
feat(x): add X (Twitter) adapter (#682)
## summary
new `@chat-adapter/x` adapter for X (Twitter), built on the X API v2 and
the X Activity API. write bot logic once and reply to mentions, hold DM
conversations, post from the account, and like posts, like the other
Chat SDK adapters
what it supports:
- reply to public mentions (`post.mention.create`) and top-level posts
via `channel.post`
- send and receive direct messages (`dm.received` / `dm.sent`)
- edit and delete owned posts, delete own DM events
- likes as the only reaction (`emoji.heart` or `"like"`)
- buffered streaming: accumulates an LLM stream and posts once instead
of post+edit churn on a public timeline
- OAuth 2.0 user context with managed token refresh (rotating refresh
token persisted in the state adapter, optional AES-256-GCM encryption)
- webhook CRC and `x-twitter-webhooks-signature` verification
key design decisions:
- DMs are threaded by the other participant's user id (`x:dm:{userId}`)
because X DM webhooks carry no conversation id, only participants
- OAuth 2.0 only at runtime: DM send and read are verified to work on
OAuth 2.0 user tokens, so no OAuth 1.0a in the adapter (subscription and
webhook setup is one-time and handled in the X developer console)
- parsers were written against real captured payloads: mentions use the
v2 shape (author hydrated in `includes.users`), DMs use the legacy
Account Activity shape (`direct_message_events`,
`message_create.message_data`, a `users` map, and no conversation id)
also includes the `chat/adapters` catalog entry, docs page, CLI scaffold
spec, and `sample-messages.md` with real captured payloads
<details><summary>usage</summary>
```typescript
import { Chat } from "chat";
import { createXAdapter } from "@chat-adapter/x";
const bot = new Chat({
userName: "mybot",
adapters: { x: createXAdapter() },
});
bot.onNewMention(async (thread, message) => {
await thread.post(`hi @${message.author.userName}!`);
});
bot.onDirectMessage(async (thread) => {
await thread.post("hello from X");
});
```
</details>
## test plan
- adapter unit tests pass against the real captured payload shapes, with
regression tests for author-from-`includes` (mentions) and the legacy
`direct_message_events` shape (DMs)
- real captured `post.mention.create` and `dm.received` payloads
verified end-to-end through `handleWebhook`: signature verification,
routing, author resolution, and participant threading, plus
bad-signature rejection returns 401
- every write and read path fired live against the X API through the
adapter: top-level post, reply to a mention, like and unlike, edit,
delete, DM send, DM read, DM delete
- OAuth 2.0 managed token refresh exercised live (access and refresh
token rotation)
---------
Signed-off-by: dancer <josh@afterima.ge>
|
||
|
|
6de45723ef |
fix(discord): Implement rehydrateAttachment (#679)
## Summary The Discord adapter never implemented `rehydrateAttachment`, so consumers couldn't rebuild an attachment's `fetchData` after a message was serialized and restored, and inbound Discord attachments couldn't be downloaded once rehydrated. Every other URL/media adapter implements it. Discord attachment URLs are directly fetchable and survive serialization, so `fetchData` is rebuilt to fetch the URL (signed params preserved, no auth header — the links are pre-signed), reading `fetchMetadata?.url ?? attachment.url` like the other URL-based adapters. Returns the attachment unchanged when there's no URL. Adds unit tests and a changeset. ## Test plan - `pnpm --filter @chat-adapter/discord test` — 248 passing - `pnpm validate` Signed-off-by: marsxiang5902 <marsxiang5902@gmail.com> |