mirror of
https://github.com/vercel/chat.git
synced 2026-09-14 18:32:29 +08:00
changeset-release/main
168 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2e2426d17e |
feat(teams): add installation lifecycle events (#914)
## summary - add `onInstalled` and `onUninstalled` handlers for Teams personal, group chat, and team installations, including upgrades that add or remove the bot from the app manifest - expose a persistable `event.channelId` for proactive messages through `bot.channel(channelId).post()`, alongside raw platform metadata for application-owned persistence - route outbound operations through the service URL encoded in each thread, preserving the explicit `apiUrl` override - document stable bot, tenant, and team installation keys separately from selected-channel destinations, and clean up for both `remove` and `remove-upgrade` - exercise the documented persistence example with regressions for upgrade cleanup, team-scoped removal, reinstall replacement, tenant isolation, and personal/group conversations closes #847 --------- 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> |
||
|
|
ea025af7ac |
feat(postgres): allow migration-managed schemas (#913)
Adds `autoCreateSchema: false` so applications can manage PostgreSQL
tables and indexes through migrations and run the bot with restricted
database permissions.
```ts
createPostgresState({
url: process.env.POSTGRES_URL,
autoCreateSchema: false,
});
```
Automatic schema creation remains enabled by default. The docs include
the migration SQL and required grants. The expired-cache behavior
reported in the issue is already fixed in the base branch.
Closes #722
---------
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
|
||
|
|
056d8830b2 |
feat(history): support uncapped per-user retention (#904)
## Summary Allow `history.user.maxPerUser: false` and legacy `transcripts.maxPerUser: false` to disable count-based eviction. Apps that retain complete user history can keep entries beyond the default 200. Numeric limits still trim older entries, and retention TTL remains independent. ## Test plan Added coverage for retaining 205 entries with no cap, the default 200-entry cap, and legacy configuration merging. The existing numeric-limit test remains. `pnpm validate` passed. ## 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> |
||
|
|
4a0b5c0c3f |
feat(cards): add button tooltips and a card width hint (#895)
Buttons can now show hover text, and a card can ask to be rendered wider
than usual. Both are small hints: Teams renders them, and every other
adapter leaves the card exactly as it was before.
### Button tooltips
`Button` and `LinkButton` take an optional `tooltip`. On Teams it
appears when someone hovers over the button.
```tsx
<Card title="Deploy request">
<Actions>
<Button id="approve" style="primary" tooltip="Ships this build to production">
Approve
</Button>
<LinkButton url="https://example.com/build/1234" tooltip="Opens the build log in your browser">
View build
</LinkButton>
</Actions>
</Card>
```
The same option is available on the plain builder functions:
```ts
Button({ id: "approve", label: "Approve", tooltip: "Ships this build to production" })
```
Tooltips survive the `callbackUrl` flow too. When a button's callback
URL is swapped for a token before the card is sent, every other field on
the button is kept, so a tooltip on a callback button shows up just like
one on a regular button.
### Full-width cards
`Card` takes an optional `width`, either `"default"` or `"full"`. Teams
draws a `"full"` card wider than its usual size, which suits tables and
digests. It does not stretch the card across the whole chat pane, that
is how Teams defines full width.
```tsx
<Card title="Weekly digest" width="full">
<Table headers={["Service", "Uptime"]} rows={[["api", "99.98%"], ["web", "99.95%"]]} />
</Card>
```
### What Teams receives
- `tooltip` becomes the `tooltip` on the Adaptive Card action, for both
submit and open-URL buttons.
- `width="full"` becomes `msteams: { width: "full" }` on the card.
- The card now declares Adaptive Card version 1.5, which is the version
that introduced action tooltips. Teams accepts cards up to 1.6 for bots,
so nothing changes for existing cards beyond the version number.
- The runtime-free `@chat-adapter/teams/cards` helpers understand both
new fields as well, so apps that build Teams cards without the full
adapter get the same result.
### Why only Teams
Slack and Google Chat have no hover text for buttons. They do have
screen-reader labels, but those replace the button text for assistive
technology rather than adding to it, so mapping a tooltip onto them
would change what a screen reader announces. The fields are documented
as Teams-only for that reason.
### Small cleanup along the way
The JSX runtime used to decide whether a set of props belonged to a
`Card` by checking for a fixed list of prop names. Any new `Card` prop
that was not on that list was silently dropped. Since `Card` is the only
component left once every other one has been matched, the props are now
used directly and the list is gone.
Docs for both props are on the cards page and in the API reference.
---------
Signed-off-by: Mohammed Mansoor Ahmed <mansoorahmed.mohammed@gmail.com>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
|
||
|
|
f485255bcf |
fix(adapters): harden webhook tenant isolation (#877)
Multi-workspace Slack now ignores commands and interactions when their installation cannot be found, and channel names stay isolated per workspace. Google Chat no longer learns its identity from incoming mentions, and forward history reads use bounded native pagination. Webhook logs avoid message content. The example app protects preview routing, records only successful verified deliveries, and caps recording size and retention. --------- Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com> Signed-off-by: dancer <josh@afterima.ge> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: dancer <josh@afterima.ge> |
||
|
|
b7c9316bfd |
fix(chat): tighten conversation boundaries (#875)
Agent tools now keep user profile lookups behind approval and apply conversation scope to typing indicators. Discord thread targets are checked against their parent channel, and private slash-command follow-ups stay private. Twilio direct messages no longer share history or scope across recipients. Queued messages restore their own conversation context before handlers run. Callback tokens are bound to their action and conversation, expire sooner, and can only be used once. Link preview metadata is clearly marked as untrusted and bounded before it reaches an AI prompt. --------- Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com> Signed-off-by: dancer <josh@afterima.ge> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: dancer <josh@afterima.ge> |
||
|
|
75cadbf9aa |
feat(twilio): add RCS support for interactive inbound and rich outbound (#590)
Extend the Twilio adapter with RCS webhook parsing, Content API integration, and card-to-template mapping with SMS fallback. --------- Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
169788b65a |
feat(chat): introduce unified History API with user, thread, and chan… (#592)
Adds `bot.history` as the canonical entry point for message history,
with three scopes: `user`, `thread`, and `channel`. `bot.transcripts`
stays as a deprecated alias, so nothing breaks.
## Why
History access was spread across `bot.transcripts`, `thread.messages` /
`thread.allMessages`, and per-adapter calls. `bot.history` puts the
promise-based read paths in one place, and the AI tools
(`fetchMessages`, `fetchChannelMessages`, `listThreads`) now route
through it.
## User scope
Cross-platform per-user persistence, identical in surface to
`bot.transcripts`:
```typescript
const bot = new Chat({
adapters: { slack, telegram },
state,
history: {
user: {
identity: ({ author }) => author.email ?? null,
retention: "30d",
maxPerUser: 200,
},
},
});
await bot.history.user.append(thread, message);
const entries = await bot.history.user.list({ userKey, limit: 20 });
await bot.history.user.delete({ userKey });
```
The new `toPromptEntries` helper turns those entries into `{ role,
content }` messages for an LLM call:
```typescript
import { toPromptEntries } from "chat";
const entries = await bot.history.user.list({ userKey });
const { text } = await generateText({
model,
messages: toPromptEntries(entries),
});
```
## Thread scope
Single-page reads and an auto-paginating generator:
```typescript
// One page, newest messages by default
const { messages, nextCursor } = await bot.history.thread.list(thread.id, {
limit: 20,
});
// Everything, oldest first, pagination handled for you
for await (const msg of bot.history.thread.collect(thread.id, { limit: 50 })) {
console.log(msg.text);
}
```
## Channel scope
```typescript
// Top-level channel messages (not thread replies)
const { messages } = await bot.history.channel.listMessages("slack:C123", {
limit: 20,
});
// Thread listings
const { threads } = await bot.history.channel.listThreads("slack:C123");
// Threads together with a page of messages each
const result = await bot.history.channel.listThreadsWithMessages("slack:C123", {
maxThreads: 5,
messagesPerThread: 10,
});
```
## Semantics
The read paths are strict about where data comes from:
- The adapter named in the ID prefix must be registered. A typo'd or
unknown prefix throws instead of reading as an empty conversation.
- The SDK-side `ThreadHistoryCache` only serves adapters that persist
history there (`persistThreadHistory: true`, e.g. Telegram, WhatsApp).
For every other adapter the platform response is authoritative, so an
empty page is a real empty page, and a `cursor` always returns the
adapter's response as-is.
- Cache reads honor the same windows as adapter reads: backward
(default) gives the newest N, forward the oldest N, and `collect()`
yields the oldest N on both paths.
- `channel.listMessages` throws a capability error on adapters without
`fetchChannelMessages` (persisting adapters are served from the
channel-keyed cache instead), and `listThreadsWithMessages` fetches
per-thread pages through `history.thread.list` a few threads at a time
to stay inside platform rate limits.
## Migration
```typescript
// Before
const bot = new Chat({
identity: ({ author }) => author.email ?? null,
transcripts: { retention: "30d", maxPerUser: 200 },
});
await bot.transcripts.append(thread, msg);
// After
const bot = new Chat({
history: {
user: {
identity: ({ author }) => author.email ?? null,
retention: "30d",
maxPerUser: 200,
},
},
});
await bot.history.user.append(thread, msg);
```
You can migrate one field at a time: when both `history.user` and the
legacy `transcripts` block are set they merge, with `history.user`
winning field by field, so settings left on `transcripts` keep applying
until you move them. `TranscriptEntry` is deprecated in favour of
`HistoryEntry` (also exported as `UserHistoryEntry`); all deprecated
names keep working in the current major version.
## Included
- New `packages/chat/src/history/` module with unit tests for every
scope
- AI tools rewired to `bot.history`, keeping their scope guards
- The nextjs example uses the new APIs throughout, with Thread History
and Channel History test buttons that exercise every scope
- Docs: `/docs/history` guide, `/docs/api/history` reference,
deprecation callouts on the transcripts pages
- Changeset (`minor` for `chat`)
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
|
||
|
|
5b538f6f21 |
fix(chat): keep thread locks alive during long handlers (#821)
- renew a held thread or channel lock every 10 seconds while a locking concurrency strategy is running - stop the heartbeat before releasing the lock, and handle extension failures without unhandled rejections - add regression coverage proving `queue`, `burst`, and `debounce` remain serialized when a handler exceeds the 30-second lock TTL - keep the existing short TTL, so a crashed process still releases its lock automatically Mosoo Agents hit this with Chat SDK's Telegram adapter while waiting on long-running Codex Agent handlers. Once a handler crossed 30 seconds, a later Telegram message could acquire an expired channel lock and run concurrently on the same conversation. Fixes #685. --------- Signed-off-by: Yevanchen <cyefan2@gmail.com> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
26a06ca51d |
feat(telegram): treat a reply to the bot as a mention (#834)
Based on #833. In a group a bot only sees messages that address it, and people address a bot by replying to it as often as by typing its handle. The adapter reported `isMention` for the handle but not for the reply, so a bot went quiet the moment the conversation moved to replies. `mentionOnReply` turns that on. **Off by default** — the flag changes which messages report `isMention`, and a bot that deliberately answers only explicit mentions should keep the stricter behaviour. It also reads `TELEGRAM_MENTION_ON_REPLY`, so a deployment can set it without code, and the key is declared in the adapters catalog. The check runs before the empty-text guard, so a reply carrying only a photo or a document counts too. --------- Signed-off-by: grootbro <vadim@ravefox.dev> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
500b7e6d2c |
fix(web): prevent tool approval bypass via client-supplied messages array (#857)
Hardens two trust boundaries reported against the framework: the web adapter derived conversation state from the client-supplied `body.messages` array, and the AI SDK write tools skipped the conversation scope check that read tools already enforced. ## Web adapter: client-supplied messages `handleWebhook` previously accepted the full `useChat` `messages` array from the browser. A client could forge tool-call and approval parts in it, and handlers reading `message.raw` would see that forged state as if the server had produced it. The adapter now: - consumes only the latest user message and ignores the rest of the array - strips tool parts from that message, so forged tool-call or approval state never reaches handlers; text, file, and custom `data-*` parts pass through to `message.raw` unchanged - returns 400 when nothing usable remains after stripping - no longer passes `originalMessages` to `createUIMessageStream` (nothing registers `onFinish`, so it was never consumed; prior turns come from the state adapter via `persistMessageHistory`, never from the request body) ## AI SDK tools: scope on writes `createChatTools` now runs the same scope guard on write tools that read tools already used. A thread or channel id the model supplies that resolves outside the scoped conversation is rejected before the write executes. The guard is threaded through each tool factory (`ToolOptions.guard`) rather than wrapped around `execute`, so it is typed against each tool's input schema and a future tool can't ship unguarded. `sendDirectMessage` targets a user id rather than a conversation, so the guard has nothing to check it against; it stays gated by approval, and the docs now say so explicitly. --------- Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
2ce2be008f |
feat(slack): add Agent Sessions lifecycle and native stop (#862)
Migrates Slack's `agent_view` integration to the Agent Sessions
lifecycle while preserving the legacy `assistant_view` compatibility
path.
- Adds `agents.sessions.setStatus` and `agents.sessions.rename` support
for processing, active, suspended, and closed sessions.
- Handles `agent_session_stopped` without taking the message lock,
clears Slack's processing state, and dispatches `onAgentSessionStopped`.
- Adds cross-process turn cancellation through the configured state
adapter and exposes the active turn as `thread.signal`.
- Handles `agent_session_title_changed` and automatically titles new
agent conversations from their root message, with a configurable
resolver.
- Propagates `session_status` through native stream completion and
supports suspended human-in-the-loop turns.
- Updates Slack manifests, examples, API docs, fixtures, and migration
guidance for the February 2027 `assistant_view` retirement.
Configure the Agent messaging experience and optional title resolver:
```ts
const slack = createSlackAdapter({
agentView: true,
sessionTitle: ({ text }) => text.split("\n", 1)[0]?.slice(0, 80) ?? null,
});
```
Pass the thread signal into model generation so Slack's native stop
button cancels upstream work as well as message delivery:
```ts
bot.onDirectMessage(async (thread, message) => {
await thread.startTyping();
const result = await agent.stream({
prompt: message.text,
abortSignal: thread.signal,
});
await thread.post(result.fullStream);
});
```
React to session lifecycle events:
```ts
bot.onAgentSessionStopped(async (event) => {
await releaseExternalResources(event.threadId);
});
bot.onAgentSessionTitleChanged(async (event) => {
await syncTitle(event.threadId, event.title);
});
```
Leave a stream suspended when the agent needs user input or approval:
```ts
await thread.post(
new StreamingPlan(result.fullStream, {
sessionStatus: "suspended",
})
);
```
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
|
||
|
|
eddcd7e46b |
fix(telegram): return portable file data (#828)
## Summary Telegram file downloads already receive their bytes from the Web Fetch API as an `ArrayBuffer`, but the adapter immediately converts them with `Buffer.from(...)` before returning. That conversion is unnecessary for consumers that accept web-standard binary data, and it throws when the Node `Buffer` global is unavailable. The [Fetch standard](https://fetch.spec.whatwg.org/#dom-body-arraybuffer) defines `Response.arrayBuffer()` as returning an `ArrayBuffer`; Cloudflare Workers exposes the [Fetch API natively](https://developers.cloudflare.com/workers/runtime-apis/fetch/), while `Buffer` belongs to its [Node.js compatibility surface](https://developers.cloudflare.com/workers/runtime-apis/nodejs/buffer/). This change returns the fetched `ArrayBuffer` directly from Telegram. The shared `Attachment.fetchData` and protected Telegram method use `Buffer | ArrayBuffer` so existing adapters and subclasses that return `Buffer` remain source-compatible. The two consumers of that contract now accept the portable value: `chat/ai` passes `ArrayBuffer` directly to the AI SDK, and the X adapter normalizes either type at its Buffer-based upload boundary. The public file documentation and patch changesets are updated with the same contract. The downstream evidence is a pnpm patch in the private Calories Cloudflare Workers consumer at `patches/@chat-adapter__telegram@4.36.0.patch`. Its portability hunk changes `downloadFile` from `Promise<Buffer>` to `Promise<ArrayBuffer>` and changes `Buffer.from(await response.arrayBuffer())` to `response.arrayBuffer()`; the other Telegram hunks in that patch are already upstream and are intentionally excluded here. ## Test plan - `pnpm validate` - `pnpm --filter @chat-adapter/telegram test` (269 tests) - `pnpm --filter @chat-adapter/telegram typecheck` - `pnpm --filter chat test` (1,131 tests) - `pnpm --filter chat typecheck` - `pnpm --filter @chat-adapter/x test` (222 tests) - `pnpm --filter @chat-adapter/x typecheck` - Added a regression test that removes the global `Buffer`, exercises Telegram's mocked `getFile` and file-fetch path, and asserts the returned bytes are an `ArrayBuffer`. The runtime proof is limited to the isolated download seam under Node with `Buffer` removed. This PR does not claim a deployed no-compatibility Cloudflare Worker or a live Telegram end-to-end request. ## Checklist - [x] All commits are signed and verified - [x] All commits are signed off for the DCO (`git commit -s`) - [x] `pnpm validate` passes - [x] Changeset added (or N/A — see [CONTRIBUTING.md](./CONTRIBUTING.md)) - [x] Documentation updated (or N/A) --------- Signed-off-by: onmax <maximogarciamtnez@gmail.com> |
||
|
|
16ea171e68 |
fix(chat): preserve thread id when editing channel messages (#848)
## summary - preserve adapter-returned thread ids on messages returned by channel message edits - prevent edited messages from falling back to the parent channel id - add regression coverage for adapters that return a thread id when posting a channel message Signed-off-by: dancer <josh@afterima.ge> |
||
|
|
c4a359e7e9 |
fix(telegram): require webhook verification by default (#858)
## summary - require `secretToken` when Telegram resolves to webhook mode - reject unverified messages and callback queries before dispatch - add `allowUnverifiedWebhooks` as an explicit escape hatch for local fixtures or trusted upstream verification - preserve polling without requiring webhook credentials - deduplicate every accepted webhook update - update adapter docs, configuration metadata, and integration fixtures --------- Signed-off-by: dancer <josh@afterima.ge> |
||
|
|
929878b56f |
fix(chat): allow link button IDs in JSX (#838)
- Accept the documented optional `id` prop in the `LinkButton` JSX
runtime guard.
- Preserve the action ID in the resulting `LinkButtonElement`.
## Discovery
We found this when Omniagent’s Slack sign-in card used the documented
`<LinkButton id="…" url="…">` API and Chat SDK’s preview renderer threw
`LinkButton requires a 'url' prop` despite receiving one.
## Root cause
The `!('id' in props)` check was introduced when `LinkButton` did not
support IDs, as a structural distinction from `Button`. Stable
link-button IDs were later added in #598 across `LinkButtonProps`,
`LinkButtonOptions`, `LinkButtonElement`, JSX resolution, adapters, and
the public documentation, but the old JSX guard was not updated.
Component identity is already established by `type === LinkButton`
before this guard runs. Requiring a string `url` is therefore sufficient
and restores the intended API without changing button dispatch or
adapter behavior.
Signed-off-by: bryan-hunter <bryan.hunter@vercel.com>
|
||
|
|
6cb933ebe2 |
fix(chat): isolate channel-scoped queue dispatch by thread (#832)
## summary - dispatch queued, debounced, and burst messages using the dequeued message's thread id instead of the lock holder's thread id - restrict skipped message context and queue logs to the dispatched message's thread - prevent subscriptions, mentions, state, and replies from crossing thread boundaries - add regressions for queue and debounce with channel-scoped locks --------- Signed-off-by: dancer <josh@afterima.ge> |
||
|
|
764e4759bd |
fix(slack): preserve pasted tables in message content (#817)
- parse Slack table blocks from both top-level blocks and attachment blocks - preserve pasted spreadsheet data in formatted mdast and plain message text - support rich text, raw text, and numeric table cells - ignore malformed table blocks without dropping valid content - fixes #803 --------- Signed-off-by: dancer <josh@afterima.ge> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
3bbf3ff542 |
fix(telegram): make native draft streaming opt-in (#822)
- use post-and-edit streaming by default to avoid leaked draft previews in Telegram clients - add `nativeStreaming: true` for explicitly enabling native draft previews in private chats - preserve existing native streaming behavior when enabled - document the client compatibility tradeoff - closes #782 before: private chat streams used native Telegram drafts by default, which could remain visible over the final message on Telegram macOS after: streams use post-and-edit by default across Telegram clients, while native drafts remain available as an opt-in --------- Signed-off-by: dancer <josh@afterima.ge> Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |
||
|
|
83ede7eab2 |
feat(chat): add message reply support (#819)
- add `thread.reply()` for sending messages with native references to existing messages - accept either a message object from the same thread or a message id as the reply target - support text, markdown, AST, cards, files, and buffered streams - add WhatsApp contextual replies using the Cloud API `context.message_id` field - apply reply context only to the first outgoing message when content is split across multiple sends - preserve the target message through sent message edits and thread history - throw `NotImplementedError` for adapters without native reply support - document the API and add message replies to the adapter feature matrix fixes #786 --------- Signed-off-by: dancer <josh@afterima.ge> Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: Aradhya C P <135510032+aradhyacp@users.noreply.github.com> |
||
|
|
18d4a230d7 |
feat(chat): add mark as read support (#820)
- add `thread.markAsRead()` for the current message, an explicit `Message`, or a message ID - expose read receipts as an optional adapter capability with explicit unsupported and thread mismatch errors - support WhatsApp read acknowledgements, Messenger `mark_seen`, and XChat read watermarks - preserve automatic XChat receipts while allowing manual timing and surfacing explicit failures - document provider-specific behavior and capability support - closes #785 --------- Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: Aradhya C P <135510032+aradhyacp@users.noreply.github.com> |
||
|
|
0f24cc3062 |
feat(chat): preserve replied-to message context (#802)
## Summary - add optional, normalized `Message.replyTo` context that survives JSON and workflow serialization, queue rehydration, thread history, and `SentMessage` reconstruction - populate it from Telegram's `reply_to_message`, including combined media groups, so handlers don't need raw Telegram payloads - keep the core contract adapter-neutral while Telegram owns only its platform mapping, allowing other adapters to populate it when they receive full replied-to messages Signed-off-by: onmax <maximogarciamtnez@gmail.com> |
||
|
|
bdeb2bf1b1 |
fix(workflow): isolate chat serializers from node runtime (#806)
## Failure Workflow SDK `5.0.0-beta.40` produces an invalid workflow bundle when a Chat SDK serializable class such as `Message`, `ThreadImpl`, or `ChannelImpl` crosses a workflow step boundary. The Workflow compiler imports the emitted module containing each class to register its `@workflow/serde` methods. In Chat SDK `4.37.0`, tsup emits those classes in `dist/index.js`. The root entry also imports the conversation-scoping implementation added in #751, which uses `AsyncLocalStorage` from `node:async_hooks`. Serializer registration therefore pulls Node-only code into the sandboxed workflow bundle before any workflow or step executes. Build warning: ```text Serde warning for classes "ChannelImpl", "Message", "ThreadImpl": Workflow bundle contains Node.js built-in imports: async_hooks. These will fail at runtime in the workflow sandbox. ``` Deployed workflows then fail during module initialization: ```text var import_async_hooks = require("async_hooks"); ^ ReferenceError: require is not defined ``` ## Minimal reproduction ```json { "dependencies": { "chat": "4.37.0", "workflow": "5.0.0-beta.40" } } ``` ```ts import { Message } from "chat"; async function createMessageStep(value: string): Promise<Message> { "use step"; return new Message({ id: "message", threadId: "slack:C123:123.456", text: value, formatted: { type: "root", children: [ { type: "paragraph", children: [{ type: "text", value }], }, ], }, raw: {}, author: { userId: "U123", userName: "user", fullName: "User", isBot: false, isMe: false, }, metadata: { dateSent: new Date(), edited: false }, attachments: [], }); } export async function testWorkflow(value: string): Promise<string> { "use workflow"; const message = await createMessageStep(value); return message.text; } ``` Running `workflow build` on `4.37.0` emits the warning; deploying the output produces the runtime failure above. ## Fix - Add a dedicated `chat/serialization` package entry for `Message`, `ThreadImpl`, `ChannelImpl`, `reviver`, and their serialized DTO types. - Make serializer code a second tsup entry and explicitly enable splitting. The serializer-bearing classes are now emitted into a shared chunk with no dependency on `Chat` or its Node-only conversation context. - Preserve the existing root exports and automatic `@workflow/serde` behavior. Existing `import { Message } from "chat"` workflow code remains valid. - Add a post-build module-graph assertion that fails if any emitted serializer registration can transitively import a Node.js builtin. - Test against Workflow SDK `5.0.0-beta.40`, the compiler version that exposed the invalid bundle. - Add a minor changeset for the fixed-version Chat SDK packages, producing the `4.38.0` release line. After the change, the emitted serializer classes live in a sandbox-safe shared chunk while `AsyncLocalStorage` remains in a separate Node runtime chunk. The exact reproduction compiles successfully with `5 steps, 1 workflow` and no Serde warning. ## Control cases The failure requires a serializable Chat class to cross a durable boundary. These cases were already safe and remain unchanged: - `AsyncLocalStorage` used entirely inside a `"use step"` function. - A Chat `Message` created and consumed within one step while returning plain data. - Request handlers that convert Chat objects to plain workflow DTOs before starting a workflow. - `@vercel/sandbox` used entirely inside a step. ## Validation - Committed beta.40 reproduction fixture: type-correct and compiled during every Chat package build with no Node builtin / Serde warning. - Emitted serializer module graph: no transitive Node.js builtins. - Chat package: 1,113 tests pass. - Chat package typecheck passes. - Repository formatting and lint checks pass. - Package build passes. Full repository validation reaches the pre-existing `knip` baseline and reports unrelated unused dependencies and unlisted binaries in examples and adapter packages. --------- Signed-off-by: bryan-hunter <bryan.hunter@vercel.com> |
||
|
|
c3b5a08e7e |
fix(gchat): bind Pub/Sub push verification to a configured identity (#797)
## summary Pub/Sub push verification checked the token's `aud` and nothing else. [Google's guidance](https://docs.cloud.google.com/pubsub/docs/authenticate-push-subscriptions) is explicit that signature and audience verification are not sufficient on their own, and that the `email` and `email_verified` claims must be checked alongside them adds `pubsubServiceAccountEmail` (env `GOOGLE_CHAT_PUBSUB_SERVICE_ACCOUNT_EMAIL`), the identity in the subscription's push auth settings. a push is accepted only when `email_verified` is true and `email` matches exactly. when the option is unset, pushes are rejected rather than trusted on their audience alone direct webhooks are untouched, and the project-number path already bound to an exact issuer ### how it happened `verifyBearerToken` took the claim validator as an optional parameter, so a call site could simply omit it, and the Pub/Sub one did while the direct-webhook one did not. that is now required: ```diff - validatePayload?: (payload: { + validatePayload: (payload: { ``` both call sites pass one and the type system enforces it, so the omission cannot recur ## test plan - a token from a different service account is rejected - a token is rejected when no identity is configured - a token is rejected when `email_verified` is not true - a token with no `email` claim is rejected - a matching identity with a verified email is accepted - direct-webhook and project-number verification are unchanged docs cover the new option in the README and adapter page, including the push-subscription authentication step that produces the token |
||
|
|
2a2b2c5500 |
feat(instagram): add native DM adapter (#770)
Adds a first-party Instagram Direct Messages adapter backed by Meta's
Instagram API with Instagram Login.
- Verifies webhook challenges and HMAC signatures, then normalizes DMs,
story replies, media, quick replies, postbacks, and reactions.
- Sends plain text, cards, quick replies, typing indicators, URL
attachments, and uploaded media through `graph.instagram.com`.
- Maps authentication, rate-limit, and 24-hour messaging-window failures
to typed adapter errors.
- Registers Instagram in the adapter catalog, CLI scaffold, official
docs, replay suite, and Next.js example.
## Usage
```ts
import { createInstagramAdapter } from "@chat-adapter/instagram";
import { Chat } from "chat";
const bot = new Chat({
userName: "mystore",
adapters: { instagram: createInstagramAdapter() },
});
```
## Webhook
```ts
export async function POST(request: Request) {
return bot.webhooks.instagram(request);
}
```
## Verification
- `pnpm --filter @chat-adapter/instagram test`
- `pnpm --filter @chat-adapter/instagram typecheck`
- `pnpm --filter example-nextjs-chat typecheck`
- `pnpm --filter example-nextjs-chat build`
- `pnpm check`
- `pnpm konsistent`
## Live Testing
<table>
<tr>
<td><img width="1440" height="2109" alt="1000000502"
src="https://github.com/user-attachments/assets/9fdb8c3b-4e41-4c81-9426-08756a5e4201"
/></td>
<td><img width="1440" height="1995" alt="1000000503"
src="https://github.com/user-attachments/assets/8a572493-c57a-4412-9049-5737aaa9dfd0"
/></td>
</tr>
</table>
Closes #729 / Co-Authored by @ivandujaut
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
|
||
|
|
0ec6a7361b |
feat(notion): add Notion comments adapter (#689)
Adds `@chat-adapter/notion`, an official adapter that lets a Chat SDK
bot take part in **Notion comment discussions** (page-level and
block/discussion threads) with the same handler code used for Slack,
Linear, GitHub, etc. Inbound events arrive via Notion webhooks
(`comment.created`) with HMAC signature verification; outbound actions
use the Comments REST API. Because Notion lets a connection edit its own
comments, the adapter supports **Post+Edit streaming**.
### Highlights
- **Webhooks** — `comment.created` verified with `X-Notion-Signature`
HMAC over the raw body (timing-safe), plus the one-time
`verification_token` handshake. Returns a fast 200 with idempotent,
state-backed dedupe.
- **Post+Edit streaming** — posts the first chunk, then `PATCH`es the
comment as tokens arrive, throttled to Notion's ~3 req/s limit (global
token bucket, `Retry-After` aware). Long bodies are split into
sequential comments to stay under the 2000-char rich-text cap.
- **Mentions** — three modes: `mention` (default; plain-text `@userName`
/ `@botUserId`), `all-comments`, and `keyword`.
- **`message.subject`** — resolves the parent page via the Pages API
(title, url, archived status, author).
- **File uploads** — up to 3 native attachments via the File Uploads API
(binary `single_part`; public URLs via `external_url` with bounded
polling); overflow and failures fall back to markdown links.
- **History** — `fetchMessages` over list-comments (open comments only),
direction-aware.
- Cards render as markdown fallback; reactions / typing / DMs are typed
no-ops or errors. Registered in the `chat/adapters` catalog and the
`create-chat-sdk` scaffold; pinned to `Notion-Version: 2026-03-11`.
### Usage
```ts
// lib/bot.ts
import { Chat } from "chat";
import { createNotionAdapter } from "@chat-adapter/notion";
import { createRedisState } from "@chat-adapter/state-redis";
export const bot = new Chat({
userName: "notion-bot",
adapters: { notion: createNotionAdapter() }, // reads NOTION_TOKEN + NOTION_VERIFICATION_TOKEN
state: createRedisState(),
});
bot.onNewMention(async (thread, message) => {
const subject = await message.subject; // parent page metadata (title, url, …)
await thread.post(`Thanks for the mention on **${subject?.title ?? "this page"}**!`);
});
```
```ts
// app/api/webhooks/notion/route.ts
import { bot } from "@/lib/bot";
export const POST = (request: Request): Promise<Response> => bot.webhooks.notion(request);
```
### Configuration
Auto-detects `NOTION_TOKEN` and `NOTION_VERIFICATION_TOKEN`, plus
optional `NOTION_BOT_USERNAME`, `NOTION_MENTION_MODE`,
`NOTION_KEYWORDS`, and `NOTION_VERSION`; everything is overridable via
`createNotionAdapter({ … })`. The docs page covers the full connection +
webhook setup (capabilities, content access, and the webhook-URL-lock
warning).
Changeset bumps `@chat-adapter/notion`, `chat`, and `create-chat-sdk`
(minor). Layered as four commits: `feat` (adapter +
catalog/scaffold/emoji), `docs`, `test`, `chore(example)`.
---------
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: dancer <josh@afterima.ge>
|
||
|
|
4ac0455134 |
feat(chat): add message update and delete lifecycle callbacks (#788)
## summary adds `onMessageUpdated` and `onMessageDeleted`, so a bot can react when a message is edited or removed. Slack dispatches both today; other adapters can opt in later supersedes #549, which was verified there against real Slack webhooks. reopened from a branch in this repo with the original commits preserved and signed ```typescript bot.onMessageUpdated(async (thread, message, previousMessage) => { await mirror.update(message.id, message.text); }); bot.onMessageDeleted(async (event) => { await mirror.remove(event.messageId); }); ``` both are lifecycle events: they never route through `onNewMessage`, `onNewMention`, or `onSubscribedMessage`, and the concurrency strategies do not apply ### notes - **the bot's own edits are filtered.** slack sends a `message_changed` for every `chat.update`, and post-and-edit streaming calls it once per delta, so without this a single streamed reply would call the handler back repeatedly on its own message - **`previousMessage` is forwarded on edits.** slack sends the pre-edit message and it was being dropped. an edit handler usually needs the before to know what changed, so it is the optional third argument - **the two shapes differ deliberately.** an edit carries a full replacement message, so it gets `(thread, message, previousMessage?)`. a delete has no message, only the id of what was removed, so it gets an event. use `chat.thread(event.threadId)` when a delete handler needs one - **one thread id helper** now serves message, edit, and delete, so an edit cannot resolve to a different thread than the message it edits ## test plan core: - an edit dispatches to `onMessageUpdated` and not to the normal message handlers - the handler receives the pre-edit message as its third argument - the bot's own edits are skipped - a delete dispatches with normalized event data - both run inside the active conversation, so read tools built in these handlers stay scoped slack: - `message_changed` dispatches as an update, `message_deleted` as a delete - `previous_message` is forwarded, and left undefined when slack omits it - hidden unfurl updates stay ignored, hidden real edits still dispatch - message, edit, and delete resolve to one thread id in a flat DM and in a threaded `agent_view` DM verified against a real slack workspace over socket mode: editing and deleting a DM both routed to the same thread id as the original message --------- Co-authored-by: Miłosz Lenczewski <m.lenczewski@tidio.net> |
||
|
|
7a1922357c |
fix(gchat): bind add-on webhook verification to a configured identity (#787)
## summary
endpoint-URL webhook verification accepted any `email` claim matching
the generic Workspace Add-on shape:
```ts
/^service-\d+@gcp-sa-gsuiteaddons\.iam\.gserviceaccount\.com$/
```
the `\d+` is a GCP project number, and service agents are
`service-{PROJECT_NUMBER}@gcp-sa-{SERVICE}...` for the project that owns
them. so that shape identifies "some Workspace Add-on", not *this* app's
add-on, and it was the only thing standing between a public endpoint URL
and a verified request. the method's own doc comment already stated the
correct invariant, that the token is only trustworthy if it was issued
to Google Chat itself
adds `workspaceAddOnServiceAccountEmail` (env
`GOOGLE_CHAT_WORKSPACE_ADDON_SERVICE_ACCOUNT_EMAIL`) and compares add-on
identities exactly. when it is unset, add-on-shaped tokens are rejected
rather than trusted by shape, with a log naming the option to set
`chat@system.gserviceaccount.com` is untouched, so standalone Chat apps
behave exactly as before. the project-number and Pub/Sub paths were
already bound to exact identities and are unchanged
### behavior
| token `email` | before | after |
| --- | --- | --- |
| `chat@system.gserviceaccount.com` | accept | accept |
| add-on shape, matches configured identity | accept | accept |
| add-on shape, different project | accept | **reject** |
| add-on shape, option unset | accept | **reject** |
<details>
<summary>why not reject at construction</summary>
refusing to initialize when the option is absent would be the
stricter-looking choice, but the adapter cannot tell Workspace Add-on
mode from config alone, it only sees `endpointUrl`. throwing there would
break every ordinary endpoint-URL Chat app. rejecting add-on-shaped
tokens at verification is the precise equivalent without the collateral
</details>
## test plan
- an add-on token matching the configured identity is accepted
- an add-on token from a different project is rejected, the case the
generic shape allowed
- an add-on token is rejected when no identity is configured
- `chat@system.gserviceaccount.com` is still accepted with no add-on
config
- suffixed and prefixed lookalike domains, an uppercase variant, and
trailing whitespace are all rejected
- a matching identity with `email_verified: false` is rejected
the two rejection cases above returned 200 before this change and 401
after
|
||
|
|
85e3d22ba1 |
fix(chat): follow-up hardening and docs for agent read-tool scoping (#774)
Follow-up hardening and updated docs for the agent read-tool scoping in `createChatTools`. ## What changed - Wrap the remaining dispatch paths (modal submit/close, assistant-thread, assistant-context, app-home, app-context, member-joined) in `runInConversation` so read tools built inside those handlers inherit the active conversation. - Log a warning when a read runs with no resolvable scope, instead of failing open silently. - Keep scoping channel-level by default; add opt-in `strictScope: true` to confine a thread scope to that thread alone (rejects sibling threads on per-thread-ACL platforms like Discord and GitHub). - Update the AI SDK tools docs to cover the channel-level default, what `scope` does and does not do, and the `strictScope` opt-in. --------- Co-authored-by: dancer <josh@afterima.ge> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
0c761f1bdd |
docs(adapters): add Dial as vendor-official adapter (#676)
Adds Dial as a vendor-official adapter — SMS, MMS, iMessage, and inbound voice-call transcripts for Chat SDK. - `vendor-official/dial.mdx` adapter page (following the Photon / Linq / Sendblue format) - catalog entry in `packages/chat/src/adapters/index.ts` with `DIAL_API_KEY` / `DIAL_FROM_NUMBER_ID` / `DIAL_WEBHOOK_SECRET` - `create-chat-sdk` scaffold spec entry - registry entry in `adapters.json` + `dial` added to vendor-official `meta.json` - integration-test doc lists + changeset Repo: https://github.com/GetDial-AI/chat-sdk-adapter · npm: `@getdial/chat-sdk-adapter` · Dial docs: https://docs.getdial.ai/integrations/agent-clients/vercel-chat-sdk The adapter maps a phone conversation to a Chat SDK thread (identified by the pair of phone numbers — Dial-owned and peer), an SMS/MMS/iMessage to a message with optional media attachments, and a completed voice call's transcript to a message on the caller's thread. Outbound sends and transcript fetches go through the official `@getdial/sdk`; inbound webhooks are HMAC-SHA256 verified against a per-subscription signing secret with constant-time compare. ### Validation - `pnpm --filter chat build` — clean - `pnpm --filter chat typecheck` — clean - `pnpm --filter create-chat-sdk typecheck` — clean - `pnpm --filter @chat-adapter/integration-tests exec vitest run src/docs-adapters.test.ts` — 361/361 passed - `pnpm check` (ultracite) — clean - `pnpm konsistent` — 34 files, no violations |
||
|
|
0b63791b66 |
fix(slack): process Socket Mode retry envelopes instead of dropping them (#667)
Fixes #666 ## Summary Both `slack_event` handlers (`startSocketMode` and `runSocketModeListener`) ack and discard every envelope with `retry_num > 0`. Slack retries an event (immediately, +1 min, +5 min) when a prior delivery wasn't acked — including events that arrived while the app had **no open socket** (restart, deploy, or Slack's routine connection refreshes). For those, the retry is the only delivery the app ever sees, so dropping it permanently loses the event (production incident details in #666). - **`@chat-adapter/slack`**: route retry envelopes through `routeSocketEvent` like first deliveries (it acks per envelope type, preserving the 3s ack window), and log them at info with `retry_num` / `retry_reason` so redelivery is observable. Duplicate protection is unchanged and sufficient: `Chat.processMessage` dedupes on `message.id` (the Slack event `ts`, identical on a retry) via `state.setIfNotExists`. - **`chat`**: raise the default `DEDUPE_TTL_MS` from 5 to 10 minutes. Slack's final retry fires ~5 minutes after the original delivery — exactly at the old TTL boundary, where the dedupe entry from the first processing could expire just before the retry arrives and cause a double-process. `dedupeTtlMs` config still overrides. Behavior note for review: apps that relied on retries being invisible will now see redelivered events flow through — deduped when already handled, processed when not. That is the intended semantic: at-least-once delivery from Slack, exactly-once handling via the SDK's dedupe. ## Test plan - Replaced the `"skips retries"` test with `"processes retries like first deliveries (dedupe drops true duplicates)"` — asserts a `retry_num: 1` envelope is acked and reaches `processMessage`. - Updated the default-TTL test to 10 minutes; the custom-`dedupeTtlMs` test is unchanged. - `pnpm validate` passes end to end (knip, check, typecheck, test, build); `pnpm --filter chat --filter @chat-adapter/slack test` = 1028 + 506 passing. Signed-off-by: tdietert <thomasd@mercury.com> |
||
|
|
3abdc69103 |
docs(adapters): add Cloudflare Agents as vendor-official state adapter (#669)
Adds Cloudflare Agents as a vendor-official **state** adapter — `agents/chat-sdk`'s `createChatSdkState()`, a Chat SDK `StateAdapter` that stores subscriptions, locks, queues, dedupe keys, thread/channel state, transcripts, and history in Durable Object SQLite via `ChatSdkStateAgent` sub-agents. - `vendor-official/cloudflare-agents.mdx` state-adapter page (Agent setup, wrangler DO migration, sharding, config, storage/cleanup) - catalog entry in `packages/chat/src/adapters/index.ts` (`group: vendor-official`, `type: state`) - registry entry in `adapters.json` + `cloudflare-agents` in vendor-official `meta.json` - integration-test doc lists + changeset Repo: https://github.com/cloudflare/agents · package `agents` (`agents/chat-sdk`) · [docs](https://developers.cloudflare.com/agents/runtime/communication/chat-sdk/) ### Not wired into the create-chat-sdk CLI This adapter runs inside a Cloudflare Worker with Durable Objects, not the generated Next.js runtime, so it is intentionally kept out of the scaffold: - added to `CLI_INCOMPATIBLE_ADAPTERS` (rejected via `--adapter`, hidden from the platform picker and e2e run, like `lark`/`matrix`) - new `listCliStateAdapters()` filters the interactive **state** picker and the `--help` adapter list (the state picker previously used raw `listStateAdapters()` and would have offered it, then thrown on selection) ### Tests - `catalog/display.test.ts` — `listCliStateAdapters`: returns only state adapters, includes `memory`/`redis`, and excludes `cloudflare-agents` while asserting it *is* in the raw catalog - `catalog/selection.test.ts` — `resolveAdapterValue("cloudflare-agents")` throws "not supported" - `cli/program.test.ts` — `buildAdapterList()` help text omits `cloudflare-agents` - existing `CLI_SCAFFOLD_SPEC covers every catalog adapter` + docs-adapters/docs-content suites cover the catalog entry, registry parity, and MDX imports ### Validation - create-chat-sdk: **178 passed**, typecheck clean - integration docs suites pass; Biome + knip clean --------- Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> |