- Add @commitlint/cli and @commitlint/config-conventional (pinned to 21.0.0)
and husky as root devDependencies.
- commitlint.config.js extends config-conventional, allows free-form scopes,
and ignores auto-generated Merge / Revert commits.
- .husky/commit-msg runs commitlint on every local commit; husky installs
automatically via the `prepare` script on `pnpm install`.
- New `commitlint` CI job in .github/workflows/ci.yml lints PR commits
(--from base.sha --to head.sha) and the most recent commit on direct
pushes to main (--last). SHAs are passed via `env:` rather than direct
`${{ … }}` interpolation to avoid the script-injection class of bug.
- CI uses `pnpm install --frozen-lockfile`, so the commitlint binary that
runs is exactly the version pinned in pnpm-lock.yaml.
- .github/CONTRIBUTING.md documents the local hook + CI enforcement.
## Summary
Refreshes the adapter docs end-to-end so every adapter — official,
vendor-official, and community — now ships hand-authored MDX, lives
under a clean URL structure, and renders on a polished
sidebar/right-rail layout dedicated to `/adapters` (the shared `/docs`
chrome is untouched).
```mermaid
flowchart LR
subgraph Before
direction TB
OB[official] --> CB[community<br/>incl. 5 vendor pages]
end
subgraph After
direction TB
OA[official] --> VA[vendor-official<br/>5 pages] --> CA[community]
end
Before -.-> After
```
### Content & routing
- **New `/adapters/vendor-official/<slug>` route** for vendor-maintained
adapters (Beeper Matrix, Photon iMessage, Liveblocks, Resend, Zernio).
Sidebar gets a third labelled group ("Vendor-Official Adapters") between
Official and Community, with a top divider matching the existing
Community treatment.
- **All 13 vendor-official + community adapters migrated** from runtime
README fetching to hand-authored MDX with rich `features:` matrices and
full body content (install, quick start, configuration, auth,
gateway/streaming, troubleshooting). README fetch stays as a fallback
for any future community adapter that hasn't been migrated yet, gated by
a new `mdxBody: true` frontmatter flag.
- **Messenger filter pages removed** (`/adapters/for/<messenger>` + the
"Browse by messenger" chip row on `/adapters`). Existing URLs
308-redirect to `/adapters`.
- **Permanent redirects** from
`/adapters/community/{matrix,imessage,resend,zernio,liveblocks}` to
their new `/adapters/vendor-official/...` paths.
- **Fixed** `/docs/adapters` and `/docs/state` so the bare pages are
accessible again — the previous catch-all redirect (`:slug*`) was
swallowing them. Switched to `:slug+` so subpath URLs still 308 while
the bare pages render.
### Visual polish
- **Adapter-only sidebar variant** (`AdaptersDocsLayout` +
`AdaptersSidebar`) with uppercase eyebrow separators, tighter rows, and
a thin themed scrollbar utility class. The shared `/docs` sidebar is
untouched.
- **Restyled `AdapterHero`**: drops the badges row + packageName, sits
the title inline with the logo, larger 17 px tagline, horizontal divider
beneath the block.
- **Restyled `PackageInstall`** as a tabbed dark single-line snippet
with a `$` prompt prefix and a copy button — replaces the previous
multi-line `CodeBlock` layout.
- **New "Deploy your chat app on Vercel" upsell card** (`<Upsell />`)
replaces the old `EditSource / ScrollTop / Feedback / CopyPage` footer
cluster on every adapter detail page.
- **Listing & messenger pages**: align the H1 to a tighter `text-4xl
sm:text-[44px]`, and the section headers to `text-base font-medium
tracking-tight` with a one-line muted lede.
### Tooling & tests
- Added `mdxBody: true` opt-in to the adapter frontmatter schema
(`source.config.ts`), and updated both detail-page handlers
(`community/[slug]` and the new `vendor-official/[slug]`) to render the
MDX body when present, falling back to README fetch otherwise.
- Refactored both detail-page handlers to flatten the body-render
branches into a `renderBody()` helper, removing the nested ternaries
that were tripping `lint/style/noNestedTernary`.
- New test file
[`packages/integration-tests/src/docs-adapters.test.ts`](https://github.com/vercel/chat/blob/docs/refresh-adapters/packages/integration-tests/src/docs-adapters.test.ts)
— **220 new assertions** covering:
- Adapter MDX frontmatter completeness, slug ↔ filename consistency, and
`type ∈ {platform, state}`.
- Vendor-official invariants: exactly the expected slugs,
`vendorOfficial: true`, `community: true`, `author`, `mdxBody: true`,
`<FeatureSupport />` rendered.
- Community invariants: `community: true` (never vendor-official),
`mdxBody: true`, `<FeatureSupport />`.
- Official invariants: never flagged, `packageName` always under
`@chat-adapter/*`.
- `adapters.json` ↔ MDX sync on `packageName` / `type` / `community` /
`vendorOfficial`.
- Extended `VALID_DOC_PACKAGES` so `docs-content.test.ts` accepts the
new vendor-official + community packages, plus `@chat-adapter/web`,
`@chat-adapter/web/react`, and `@chat-adapter/messenger`.
### Per-package AGENTS.md
- Added `AGENTS.md` to every official adapter and state adapter (14
packages), each tailored to that adapter's surface — overview, directory
layout, build/test commands, public exports, thread ID format, webhook
flow, authentication, format conversion, cards/streaming, platform
quirks, testing approach, coding conventions, and release rules.
- Added a one-line `CLAUDE.md` (`@AGENTS.md`) beside each so Claude Code
picks up the same instructions through its built-in resolver — same
convention as the root.
### Web adapter copy
- Cleaned up the Web adapter tagline (removed inline backticks) and
dropped the now-redundant "v1 scope" section from the body.
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
## summary
fixes#343
fix Discord gateway-only mode so `InteractionCreate` events are handled
directly when no interactions endpoint is configured
slash commands now defer the gateway interaction and route through the
existing slash command handler path, and button interactions defer
updates before routing through the existing action handler path
also clarifies in the Discord README that Discord sends interactions
through either the Gateway or an Interactions Endpoint URL, not both
## summary
clarifies that registered `onDirectMessage` handlers take precedence for
incoming DM messages before subscribed-message, mention, and pattern
routing
updates the direct messages, event handling, thread subscription, and
API docs so they match the current runtime behavior
adds `onDirectMessage` to the Chat API docs
fixes#432
* chore(integration-tests): add Emulate.dev Slack emulator devDeps
Add @emulators/slack, @emulators/core, and @hono/node-server as
devDependencies of the private integration-tests package. These power
the upcoming in-process Slack emulator harness used to drive the Slack
adapter against a stateful, Slack-shaped HTTP server instead of mocks.
* test(integration-tests): add Slack emulator test harness
Introduce slack-emulator-utils.ts, a test-only helper that boots the
@emulators/slack Hono app on an ephemeral 127.0.0.1 port via
@hono/node-server and pre-seeds a deterministic team / channel / bot /
human user / OAuth app. The Slack adapter is wired to it via its
existing apiUrl config; no source changes required.
Also exposes startSlackWebhookForwarder, a tiny in-process Node http
forwarder that re-signs the emulator's outbound event_callback
deliveries with x-slack-signature / x-slack-request-timestamp before
handing them to chat.webhooks.slack(...). The emulator's core
WebhookDispatcher only emits GitHub-style X-Hub-Signature-256 headers,
so this bridge is what makes inbound flows exercise the SDK's real
HMAC verification path.
The handle returns direct access to the emulator's Store and
WebhookDispatcher so tests can assert on persisted state instead of
mock call records.
* test(integration-tests): cover Slack auth, postMessage, and reactions via emulator
Add three test files that drive the SlackAdapter's outbound WebClient
calls through the in-process emulator and assert on its stateful store
rather than on mock call records:
- emulator-slack-auth.test.ts (3 tests): auth.test populates botUserId
during initialize(); explicit botUserId is respected; multi-workspace
mode skips the call entirely.
- emulator-slack-post-message.test.ts (5 tests): plain text and threaded
thread.post round-trip into the messages collection and are visible
via conversations.replies; editMessage updates via chat.update;
deleteMessage removes via chat.delete; markdown posts succeed.
- emulator-slack-reactions.test.ts (3 tests): addReaction /
removeReaction round-trip via reactions.add / reactions.remove and
show up via reactions.get; multi-user reactions accumulate correctly.
These exercise the full HTTP path against a Slack-shaped server,
catching wire-format and contract issues that pure mocks miss.
* test(integration-tests): cover Slack inbound events and OAuth v2 install via emulator
Add two test files that drive end-to-end flows previously only
verifiable against real Slack:
- emulator-slack-events.test.ts (4 tests): a human posts to the
emulator, which dispatches an event_callback to the local forwarder,
which signs the body and hands it to chat.webhooks.slack(...). The
SDK's onNewMention and onNewMessage handlers run with a live Thread
and the bot's reply lands back in the emulator. Bot self-messages
are correctly filtered. This is the only Slack adapter test in the
repo that covers the full inbound-then-outbound round-trip without
hand-crafted webhook payloads.
- emulator-slack-oauth.test.ts (4 tests): handleOAuthCallback
exchanges a real authorization code via oauth.v2.access against the
emulator's authorize/callback flow; the resulting installation is
persisted in the state adapter; invalid codes and mismatched
client_secrets are rejected; the freshly issued bot token works for
subsequent chat.postMessage calls via withBotToken.
* docs(integration-tests): document emulator-* test category
Add an "Emulator tests" entry to the package README so newcomers can
distinguish the new emulator-backed suite from the existing unit and
replay tests, and find the harness in slack-emulator-utils.ts.
* fix(integration-tests): keep full token scopes after emulator.reset()
`applyTokenSeed` (used during `reset()`) was granting only
["chat:write", "channels:read"] to seeded tokens, while the initial
`createCoreServer({ tokens })` call granted the full bot/human scope
sets. After the first `reset()` the bot token silently lost
`channels:history`, `users:read`, `reactions:read`, and
`reactions:write`, which would surface as flaky behaviour for any test
that relied on those scopes after a reset.
Unify both call sites on a single `buildTokenSeedEntries` helper so
fresh-boot and post-reset state always grant the same scopes. Add a
regression test in emulator-slack-auth.test.ts that triggers a manual
`emulator.reset()` and re-asserts that the bot token still resolves
via auth.test.
* refactor(integration-tests): reorganize Slack emulator tests under emulator/slack/
Address review feedback (visyat) by moving the flat
`emulator-slack-*.test.ts` files into a per-adapter directory:
packages/integration-tests/src/emulator/slack/
utils.ts
auth.test.ts
events.test.ts
oauth.test.ts
post-message.test.ts
reactions.test.ts
This scales cleanly as more adapter emulator suites land (e.g.
`emulator/github/`), instead of cluttering the top-level src tree with
adapter-prefixed file names.
Also hoist the duplicated `silentLogger` definition from each test
file into the shared `utils.ts`, removing five identical copies.
No behavior changes. All 20 emulator tests still pass.
* test(integration-tests): cover Slack multi-workspace token resolution via emulator
Add `emulator/slack/multi-workspace.test.ts` exercising the path that
was previously only covered by replay/mock-based tests: the adapter
runs without a hardcoded `botToken`, multiple workspaces are persisted
in the state adapter via `adapter.setInstallation(teamId, ...)`, and
inbound `event_callback`s for either team are routed to the correct
per-tenant bot token end-to-end against the shared emulator.
To make this work, the helper grew two small additions:
- `addEmulatorWorkspace(emulator, seed)` — register an additional
team + bot + channel + token on an already-booted emulator, so two
tenants live side-by-side without needing two emulator instances.
- The inbound forwarder's `augmentEventEnvelope` now accepts an
optional `resolveTeamId(envelope)` callback, defaulting to a constant
teamId for the existing single-workspace tests. The multi-workspace
test passes a resolver that looks up the dispatched event's channel
in the emulator store and returns the owning team's id, so the SDK's
per-team token resolver sees the right `team_id` on the envelope.
Three new tests: team-A routing, team-B routing, and `getInstallation`
returning null for unknown team ids. Follow-up from review feedback on
PR #477.
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* fix(adapter-telegram): handle video_note in extractAttachments
Round video messages (video_note) were silently dropped because
extractAttachments had no branch for them. Adds the video_note field
to TelegramMessage and extracts it as a "video" attachment with
width/height set to the clip's length.
Fixes#456
* test(adapter-telegram): add video_note attachment extraction test
* set up konsistent with basic initial config
* add konsistent to CI
* fix konsistent.json formatting
* fix(gchat): move GoogleChatAdapterConfig to ./types for konsistent
* fix(slack): move SlackAdapterConfig to ./types and drop Partial wrapper from createSlackAdapter
* fix(messenger): name createMessengerAdapter parameter MessengerAdapterConfig
* fix(web): rename WebAdapterOptions to WebAdapterConfig and import Adapter type in index.ts
* fix(whatsapp): align WhatsAppAdapterConfig and creator with konsistent + map kebab to PascalCase
* fix(state-memory): add MemoryStateAdapterOptions type for konsistent
* fix(state-ioredis): unify URL and client options under IoRedisStateAdapterOptions
* fix(state-redis): unify URL and client options under RedisStateAdapterOptions
* fix(state-pg): unify URL and client options under PostgresStateAdapterOptions
* chore: changeset for konsistent convention alignment
* chore: drop CHANGELOG.md from konsistent's required files list
CHANGELOG.md is generated automatically by changesets on each release —
it's never hand-authored and doesn't exist for a package until its first
release lands. Requiring it as a convention check makes CI fail
indefinitely for any newly added package, with no honest fix available
(an empty placeholder is just noise that gets overwritten on first
release).
* docs: document konsistent and package shape conventions
* fix(web): use WebAdapterConfig in WebAdapter field types after main merge
The merge of main into konsistent brought in PR #475's `protected`
field modifiers on top of the WebAdapterOptions → WebAdapterConfig
rename, leaving two stale references to the (un-imported) old name.
Switch them to WebAdapterConfig and update a stale JSDoc reference
in als.ts to match.
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <27636870+bensabic@users.noreply.github.com>
* add .nvmrc file
* follow up
* make support of Node >= 20 explicit
* update default Node version for contributing to the repo to 24
* run build-and-test CI job for both Node 20 and 24
* add changeset for package.json change
Pushes the published commit to the `release` branch so Vercel projects
configured with `release` as their production branch (docs, example app)
only deploy after a successful `changeset publish`.
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* chore: changed class function access to protected
* chore: format
* chore: added changeset
* chore(changeset): drop unchanged shared package, expand description
* refactor(adapters): keep internal state private, only protected for extension surface
Narrow scope of #475: caches, polling/runtime state, and one-shot warning
flags stay private. Methods and shared helpers (logger, formatConverter,
chat, config) remain protected as the documented extension surface.
Also fixes a typecheck failure where gchat's oauth2Client (now private)
no longer requires a portable type for the emitted .d.ts.
* test(adapters): add subclass extensibility tests
Each adapter now has a compile-time test that subclasses can access the
documented protected surface. If any of these members revert to private,
the test file fails to type-check.
* style: apply ultracite formatting to subclass tests
* style(slack): mark static cache TTL constants as readonly
These three protected static cache TTLs are configuration constants, not
mutable state. Marking them readonly prevents subclasses (the new extension
surface from this PR) from mutating values shared across every instance
in-process.
* test(adapters): document intent of subclass extensibility tests
Mirrors the inline comment from the Telegram subclass test across the other
nine adapter tests so future readers immediately understand these blocks are
type-only sentinels — they fail at typecheck (not vitest) if a member reverts
to private.
* docs(adapters): document subclassing for adapter customization
Adds a "Customizing an adapter via subclassing" section to the Adapters page
that walks through extending an official adapter to override a protected hook
(using the issue #433 Telegram processUpdate scenario as the canonical
example) and clarifies that private members are intentionally off-limits.
* refactor(linear): expose accessTokenExpiry to subclasses
The surrounding refreshClientCredentialsToken and ensureValidToken methods
are now protected, but accessTokenExpiry was kept private — meaning a
subclass overriding either method couldn't read or update the expiry without
calling super. Flipping it to protected lets subclasses fully reimplement
the token-refresh flow.
* chore(changeset): bump adapters from patch to minor
This PR adds a new, additive capability — subclassing official adapters to
override protected hooks. Per CONTRIBUTING.md, additive backward-compatible
features warrant a minor bump rather than a patch.
* docs(adapters): clarify subclassing surface stability
Correct the parenthetical describing what stays `private` (credentials are
now `protected`) and add a callout warning that the `protected` extension
surface is intentionally broader than the public API but not yet fully
stable, so subclass authors know to pin versions and prefer overriding
the smallest hook.
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* chore: Add Messenger logo to homepage
* chore: Use Messenger brand colour for logo
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* Revert "feat(slack): expose direct WebClient access via adapter.client (#471)"
This reverts commit 8366b8b0fb.
* Fix: The `createSlackAdapter()` helper function silently drops the `apiUrl` config field, so custom Slack API URLs (e.g., for GovSlack) are ignored when using the helper.
This commit fixes the issue reported at packages/adapter-slack/src/index.ts:5055
**Bug explanation:**
The `SlackAdapterConfig` interface defines an `apiUrl` field (line 166) that allows users to override the Slack Web API base URL — useful for GovSlack or self-hosted gateways. The `SlackAdapter` constructor reads this field at line 622:
```typescript
const slackApiUrl = config.apiUrl ?? process.env.SLACK_API_URL;
```
However, the `createSlackAdapter()` helper function (around line 5055) constructs a `resolved` config object that includes many fields from the user's config but omits `apiUrl`. This means when a user writes:
```typescript
createSlackAdapter({ apiUrl: "https://slack-gov.com/api/" })
```
The `apiUrl` is silently dropped and the `WebClient` is created without the custom URL. The `SLACK_API_URL` environment variable fallback still works (since it's checked in the constructor), but explicit config via the helper is lost.
This is clearly a bug — all other config fields are forwarded through the `resolved` object, and `apiUrl` was simply forgotten.
**Fix explanation:**
Added `apiUrl: config?.apiUrl,` to the `resolved` config object in `createSlackAdapter()`. This ensures the `apiUrl` value from user config is properly forwarded to the `SlackAdapter` constructor, matching the pattern used for all other optional config fields.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: visyat <vishal.yathish@gmail.com>
---------
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
* feat(slack): expose direct WebClient access via adapter.client
Mirror the Linear and GitHub adapter pattern by exposing the underlying
@slack/web-api WebClient as `adapter.client` for any Web API call not
covered by the SDK's high-level methods.
Resolution order:
1. Token from the current request context (multi-workspace webhooks,
`withBotToken()`).
2. The default `botToken` when configured as a static string or a
synchronous resolver function.
Throws AuthenticationError outside of any context in multi-workspace
mode, or when `botToken` is configured as an async resolver. For both,
bind the token explicitly with `adapter.withBotToken(token, () => ...)`.
Internally, the existing private `client` field is renamed to `_client`
so the public getter can return per-token cached `WebClient` instances.
All internal API calls continue to route through `_client.foo(await
this.withToken(...))` unchanged. Also fixes `createSlackAdapter()`
silently dropping the `apiUrl` config field, surfaced by the new
apiUrl-propagation test.
* docs(slack): document direct WebClient access
Add Slack to the "Direct client access" section of the chat-sdk.dev
docs (api/chat.mdx, usage.mdx) alongside Linear and GitHub. Update the
multi-tenant Callout to spell out both Slack constraints — request
context required in multi-workspace mode, and `withBotToken()` required
when `botToken` is an async resolver.
Add a parallel "Direct WebClient access" section to the Slack adapter
README with a usage example, the token resolution order, and the
async-resolver workaround.
* feat(example): add Channel Info button using slack.client
Demonstrate the new direct WebClient access pattern in the nextjs-chat
demo with a "Channel Info (Slack)" button. The handler resolves the
Slack adapter from the action event, reaches into
`adapter.client.conversations.info` (channels:read scope, already in
the example manifest), and renders the result as a Card with channel
name, member count, topic, purpose, and the standard flags. Falls back
to a friendly message on non-Slack platforms.
* feat(example): add Pin Message button using slack.client.pins.add
Pin the welcome card itself via `adapter.client.pins.add({ channel,
timestamp: event.messageId })` to demonstrate calling a Slack Web API
endpoint not wrapped by the SDK. Adds the required `pins:write` scope
to the example Slack manifest.
* chore(example): render channel info as a table and include num_members
Replace the Fields/Section layout in the Channel Info card with a
two-column Table for a tidier presentation, and pass
`include_num_members: true` so the Members row is actually populated
(Slack's `conversations.info` omits it by default).
* test(slack): expand coverage for adapter.client
Adds three tests:
- Cache differentiation: distinct tokens produce distinct WebClient
instances so per-workspace credentials never bleed across calls.
- apiUrl env var resolution: SLACK_API_URL is honored by the WebClient
the new getter returns (covers GovSlack-style deployments).
- End-to-end multi-workspace token routing: a real block_actions
webhook drives `processAction`, and the handler-side
`event.adapter.client.token` matches the installation's bot token —
proving the request-context-bound client works inside webhook
dispatch.
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* feat(tests): add @chat-adapter/tests test kit
New package providing Vitest factories, custom matchers, and a setup file for
people building Chat SDK adapters and bots.
Factories: createMockAdapter, createMockChatInstance, createMockState (with
working in-memory subscriptions/locks/KV/queues), createTestMessage,
mockLogger / createMockLogger.
Matchers: toHavePosted(threadId, textPattern?), toHaveDispatched(handler),
toBeSubscribedTo(threadId). Auto-register via the
'@chat-adapter/tests/setup' subpath in vitest setupFiles.
chat and vitest are peer dependencies. Adapter-specific helpers (e.g. signed
Slack webhook builders) belong in each adapter's own /testing subpath, not
in this kit.
* test(integration-tests): allow @chat-adapter/tests imports in README check
* docs: add Testing page covering @chat-adapter/tests
New content/docs/testing.mdx walks bot authors and custom-adapter authors
through the kit's factories, custom matchers, and setup file. Added under
the Usage section in the sidebar, after error-handling.
Cross-link from contributing/testing.mdx clarifying that the hand-rolled
patterns there are for repo contributors building first-party adapters,
while consumers of Chat SDK should use @chat-adapter/tests.
* test(integration-tests): allow @chat-adapter/tests imports in docs check
* fix(tests): match real Adapter.postMessage signature in toHavePosted
Adapter.postMessage is (threadId: string, message: AdapterPostableMessage)
— previously the matcher read args[0] as { id: string } and args[1] as
{ text: string }, neither of which match the actual SDK shape. The matcher's
own tests fed the same wrong shape into the mock so they passed locally
while the matcher silently failed against any real bot or adapter.
Now compares args[0] as a string threadId, and extracts a comparable string
from AdapterPostableMessage's union — strings directly, PostableMarkdown
.markdown, PostableRaw.raw, and PostableCard.fallbackText. PostableAst and
fallback-less cards aren't text-matchable; documented in the JSDoc.
Tests updated to call postMessage with the real signature and to cover
each comparable AdapterPostableMessage shape.
* test(tests): add smoke tests driving matchers against a real Chat
Construct a real `Chat` with a `createMockAdapter` + `createMockState` and
exercise `Chat.thread().post()` and `.subscribe()` end-to-end. The matchers
(toHavePosted, toBeSubscribedTo) then assert against the actual call shape
the SDK uses, so a future signature drift breaks here instead of silently
agreeing with whatever wrong shape lives in the unit tests.
This is the regression guard for the postMessage-shape bug fixed in the
prior commit: each new matcher in subsequent PRs should be paired with a
smoke case here.
* feat(tests): round out adapter mutation matchers
Adds toHaveEdited, toHaveDeleted, toHaveReactedWith, toHaveStartedTyping,
and toHavePostedToChannel — covering the common Adapter mutation surface
that bot authors assert on. Each matcher's signature was checked against
packages/chat/src/types.ts, and each is paired with a smoke case that
drives a real Chat through the corresponding Thread/Channel API so
signature drift breaks the smoke test instead of silently agreeing with
the unit tests.
Emoji matching accepts both plain strings and EmojiValue ({ name }).
Text matching reuses the same extraction rules as toHavePosted —
strings, PostableMarkdown.markdown, PostableRaw.raw, and
PostableCard.fallbackText. Documented in matcher JSDoc, README, and
the Testing docs page.
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* [slack] add support for external installation providers for bot token management
* changeset
* fix(slack): correct stale 'Connex' comment to 'Vercel Connect'
* docs(slack): expand changeset with provider details and Enterprise Grid notes
* docs(slack): document installationProvider and Enterprise Grid lookup keys
* test(slack): cover installationProvider for interactive payloads on both install types
* fix(slack): route rehydrateAttachment through installationProvider
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* fix(slack): prefer webhookVerifier over signingSecret and SLACK_SIGNING_SECRET
When `webhookVerifier` is configured, ignore both `config.signingSecret`
and the `SLACK_SIGNING_SECRET` env var. Previously a configured
`signingSecret` (or env var) would shadow the verifier the caller wired
up, which is the opposite of the intended override behavior.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(slack): update README to match new webhookVerifier precedence
Mirror the JSDoc/changeset wording: webhookVerifier wins over both
signingSecret and SLACK_SIGNING_SECRET. The previous note still claimed
signingSecret won, contradicting the behavior fixed in e77359b.
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* [messenger] add messenger (meta) platform adapter to chat sdk
- Webhook handling with HMAC-SHA256 signature verification
- Generic and Button template support for cards
- Postback, reaction, delivery/read confirmation handling
- Message caching for fetchMessages (Messenger has no history API)
- Replay tests and ~98% code coverage
Co-authored-by: Dimitar K. Nikolov <mitkodkn@users.noreply.github.com>
Co-authored-by: Ben Sabic <27636870+bensabic@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix: The `@chat-adapter/messenger` package version is `4.15.0` while all other packages in the Changesets fixed version group are at `4.27.0`, breaking the fixed versioning contract.
This commit fixes the issue reported at packages/adapter-messenger/package.json:3
**Bug explanation:**
The repository uses Changesets with a `"fixed"` configuration: `[["chat", "@chat-adapter/*"]]`. This means all packages matching these patterns must always share the same version number. Every package in the group (`chat`, `@chat-adapter/discord`, `@chat-adapter/gchat`, `@chat-adapter/github`, `@chat-adapter/linear`, `@chat-adapter/shared`, `@chat-adapter/slack`, `@chat-adapter/teams`, `@chat-adapter/telegram`, `@chat-adapter/web`, `@chat-adapter/whatsapp`, and the state packages) is at version `4.27.0`, except `@chat-adapter/messenger` which is at `4.15.0`.
This is likely because the messenger adapter was newly added to the monorepo (copied from a template or created fresh) and its version was never aligned with the rest of the fixed group. This mismatch will cause problems with the Changesets release workflow — when Changesets tries to bump versions for the fixed group, it may produce inconsistent or errored releases because one package is 12 minor versions behind the others.
**Fix explanation:**
Changed `"version": "4.15.0"` to `"version": "4.27.0"` in `packages/adapter-messenger/package.json` to align it with all other packages in the fixed version group.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: visyat <vishal.yathish@gmail.com>
* Fix: Messenger adapter env var guard only checks `FACEBOOK_APP_SECRET` but `createMessengerAdapter` requires all three env vars, causing a `ValidationError` crash at Next.js build time when only `FACEBOOK_APP_SECRET` is set.
This commit fixes the issue reported at examples/nextjs-chat/src/lib/adapters.ts:154
**Bug Analysis:**
The build failure is confirmed in the Vercel build log with:
```
Error [ValidationError]: pageAccessToken is required. Set FACEBOOK_PAGE_ACCESS_TOKEN or provide it in config.
```
The root cause is in `examples/nextjs-chat/src/lib/adapters.ts` at line ~154. The messenger adapter guard only checks for `FACEBOOK_APP_SECRET`:
```typescript
if (process.env.FACEBOOK_APP_SECRET) {
```
However, `createMessengerAdapter` (in `packages/adapter-messenger/src/index.ts`) validates and throws `ValidationError` for each of three required env vars: `FACEBOOK_APP_SECRET`, `FACEBOOK_PAGE_ACCESS_TOKEN`, and `FACEBOOK_VERIFY_TOKEN`. When only `FACEBOOK_APP_SECRET` is set in the Vercel project environment, the guard passes, `createMessengerAdapter` is called, and it throws a `ValidationError` for the missing `FACEBOOK_PAGE_ACCESS_TOKEN`. Since this code runs at module evaluation time during the Next.js build's "Collecting page data" phase, the uncaught error crashes the entire build.
This is inconsistent with other adapters in the same file. For example, the WhatsApp adapter checks both `WHATSAPP_ACCESS_TOKEN` and `WHATSAPP_PHONE_NUMBER_ID`, and the gchat/github/linear/whatsapp adapters all wrap creation in try-catch blocks.
**Fix:**
1. Updated the env var guard to check all three required environment variables (`FACEBOOK_APP_SECRET`, `FACEBOOK_PAGE_ACCESS_TOKEN`, and `FACEBOOK_VERIFY_TOKEN`) before attempting to create the adapter.
2. Wrapped the `createMessengerAdapter` call in a try-catch block (matching the pattern used by gchat, github, linear, and whatsapp adapters) so that any unexpected validation errors are caught and logged as warnings instead of crashing the build.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: visyat <vishal.yathish@gmail.com>
---------
Co-authored-by: Dimitar K. Nikolov <mitkodkn@users.noreply.github.com>
Co-authored-by: Ben Sabic <27636870+bensabic@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
* 1
* w
* 3f
* x
* ln
* gh
* sl
* t1
* u
* t2
* t3
* t4
* t5
* d
* d2
* cs
* fx
* cl
* docs: clean up subject + .client docs and restructure nav
- subject.mdx: simplify prose, drop redundant platform lists, link to MessageSubject API and getAdapter
- api/message.mdx: add MessageSubject TypeTable
- api/chat.mdx: expand getAdapter with Direct client access content
- adapters.mdx: add Parent subject and Native client rows to feature matrix
- usage.mdx: mention .client under Accessing adapters
- adapter-github/-linear READMEs: add Direct API client section
- meta.json: split Features into Messaging + Interactivity, move error-handling to Usage
- title case across messaging-cluster page titles
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* feat(chat): expose awaitable Promise from processMessage
Return the inner task as Promise<void> instead of void so streaming
adapters can await full handler completion and surface user-handler
rejections at the wire level. waitUntil semantics for existing webhook
adapters are unchanged — the SDK still tracks the work with errors
swallowed (and logged) so platforms don't retry on handler bugs.
Required by @chat-adapter/web, whose response body is the user
handler's stream.
* feat(adapter-web): add @chat-adapter/web package
A new platform adapter that lets a chat-sdk bot serve a browser chat
UI alongside Slack/Teams/Discord/etc. without writing any client-side
glue. Speaks the AI SDK UI message stream protocol, so @ai-sdk/react's
useChat and the ai-elements component library work out of the box.
- `@chat-adapter/web` — server: createWebAdapter({ userName, getUser })
- `@chat-adapter/web/react` — client: useChat() preconfigured with
DefaultChatTransport against /api/chat (override via `api`)
Defaults that matter for v1:
- `isDM: true` — every web message routes through onDirectMessage
- `persistMessageHistory: true` — chat-sdk caches each turn in the
configured state adapter so handlers can read prior context via
thread.messages / channel.messages (no platform history API exists)
- channelId === threadId — web has no separate channel concept; this
prevents cross-conversation bleed when a single user has multiple
useChat sessions
- Native `adapter.stream` implementation pumps text-deltas straight
onto the SSE response — no post+edit fallback
Out of scope for v1: cards/JSX rendering, reactions, modals, file
uploads, edit/delete, multi-tab proactive push.
* feat(example-nextjs-chat): wire up web adapter and add /chat page
- Register the web adapter in lib/adapters.ts with a demo getUser
(single shared identity — replace with NextAuth/Clerk/cookie auth
in production)
- Expose POST /api/chat backed by bot.webhooks.web (using next/after
for waitUntil)
- Add a minimal /chat page using @chat-adapter/web/react's useChat —
same bot.onDirectMessage handler that powers Slack now powers the
browser too
Bumps `ai` to ^6.0.174 to align with @ai-sdk/react@^3 (avoids dual
provider-utils versions in the workspace).
* docs: list @chat-adapter/web in registry
- Add an entry to adapters.json so the package shows up on /adapters
- Add a globe SVG to lib/logos.tsx and wire it into the icon map
- Mention the new adapter in docs/adapters.mdx
* feat(adapter-web): tighten request handling and message construction
- Reject user ids containing ':' with HTTP 400 — the character would
corrupt the thread-id round-trip through decodeThreadId
- Skip emitting text-start/text-end in postMessage when the resolved
text is empty so useChat doesn't render blank assistant bubbles
- Derive the parseMessage author from raw.role so rehydrated assistant
messages report the bot identity instead of "unknown"
- Drop the duplicate handler-error log; chat.processMessage already
logs at ERROR level
- Document the actual persistMessageHistory default (true) and the
state-cache rationale; promote the fetchMessages no-op rationale
into its JSDoc
* test(adapter-web): add direct coverage for stream()
- Aborting request.signal mid-stream short-circuits the iterator and
still writes text-end via the finally block
- Non-text StreamChunks (task_update, plan_update) are dropped without
emitting any delta
- The SentMessage returned from thread.post matches the id used in
text-start / text-end events
* docs(adapter-web): expand README into the full adapter docs page
The docs site renders each adapter's README, so flesh out
@chat-adapter/web to match the depth of @chat-adapter/slack:
authentication boundary, threading semantics, streaming,
persistence, React hook reference, configuration table,
feature matrix, and troubleshooting.
* docs(adapter-web): drop unsupported provider import from streaming example
* fix(adapter-web): validate conversationId for reserved colon character
* fix(example): show error state in web chat demo
* fix(example): add thinking indicator to web chat demo
* feat(example): redesign web chat demo with tailwind
* chore: remove redundant changeset
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: dancer <josh@afterima.ge>
* feat(chat): add Transcripts API and rename per-thread cache to threadHistory
Introduce `bot.transcripts` for cross-platform per-user message persistence.
When `ChatConfig.transcripts` and `ChatConfig.identity` are configured, every
inbound message has its `userKey` resolved during dispatch and the API exposes
`append` / `list` / `count` / `delete` keyed by that user. Backed by the
existing `StateAdapter.appendToList` primitive — every built-in state adapter
supports it with no contract changes.
Rename the existing per-thread history cache from `messageHistory` to
`threadHistory` (with backwards compat for `ChatConfig.messageHistory` and
`Adapter.persistMessageHistory`) so the two persistence layers don't share a
"messages" name. The state-adapter storage key prefix is unchanged so existing
data isn't orphaned.
`delete()` writes a tombstone via `appendToList(key, _, { maxLength: 1 })`
rather than `state.delete(key)`, because `state.delete` only addresses the
k/v namespace on every non-memory state adapter — `list()` and `count()`
filter the tombstone out so the API contract is preserved.
* docs(chat): cover Transcripts API and Conversation history
Add a Features-style "Conversation history" guide (`/docs/conversation-history`)
walking through identity resolution, the LLM-context append/list pattern,
filtering, and per-user deletion for DSR flows.
Add an API reference page at `/docs/api/transcripts` with `<TypeTable>` blocks
for `ChatConfig.transcripts`, `ChatConfig.identity`, every method on
`bot.transcripts`, and the `TranscriptEntry` shape.
Wire both into the corresponding `meta.json` files.
* example(nextjs-chat): wire Transcripts API into the AI mode handler
Replace the brittle `threadState.history` shim with `bot.transcripts.list({ ..., threadId, limit })` as the fallback context source for platforms without
`fetchMessages` (Telegram, WhatsApp). Drop the `history` field from
`ThreadState` accordingly.
Add a hardcoded `TEST_USER_KEY = "test-user"` so the API can be exercised
without juggling real user identities, plus "Show Transcripts" and
"Clear Transcripts" buttons in the welcome card so the store can be
inspected and reset from chat.
* fix(chat): tighten Transcripts API public surface and wiring
Polish on top of the Transcripts API + threadHistory rename, addressing
review concerns before merge.
Public surface (`types.ts`, `index.ts`):
- Expose `transcripts` on the `ChatInstance` interface so callers typed
against the public interface can reach `bot.transcripts`.
- Promote the `count` argument to a named `CountQuery` interface,
matching `DeleteTarget` / `ListQuery`. Exported from `index.ts`.
- Document on `TranscriptsApi.list()` that pagination is intentionally
out-of-scope — the store keeps at most `maxPerUser` entries per user.
- Reconcile the `TranscriptEntry.id` JSDoc with the implementation:
UUID assigned at append time, returned in append order, not
lexicographically sortable; use `timestamp` for cross-store ordering.
Wiring (`chat.ts`):
- Include `threadId` in the identity-resolver failure log context so
operators can correlate failures with the source thread.
Stale-reference sweep:
- Replace lingering "Messages API" / `chat.messages` /
`messages.storeFormatted` strings in shipped JSDoc with the new
`transcripts` names (these ride into `.d.ts` and are user-visible).
- Fix the dead `[Messages API](./messages.ts)` link in the existing
thread-history-rename changeset.
* test(chat): cover dual-read precedence, resolver edges, concurrent ops
Fill gaps in the Transcripts API + threadHistory rename test suite:
`chat.test.ts` (persistThreadHistory block):
- top-level `config.messageHistory` (deprecated alias) flows through
to the per-thread cache when `threadHistory` is unset
- `threadHistory` takes precedence over `messageHistory` when both are
set — pinned by asserting `appendToList` receives the new config's
`maxLength` / `ttlMs`
- both `persistThreadHistory` and `persistMessageHistory` set on the
adapter still triggers persistence
`transcripts-wiring.test.ts`:
- sync resolver returning a plain string populates `message.userKey`
- resolver returning `""` is treated as no userKey (truthy check at
the dispatch hook would silently flip if a future change moved to
`!== undefined`)
`transcripts.test.ts`:
- concurrent append/delete/append interleave preserves invariants:
`count()` and `list()` agree (no tombstone leak), no pre-delete
entry survives, and the post-delete result is bounded by the two
concurrent appends
* docs(chat): use named types in Transcripts API reference
- `formatted` rows in the AppendInput / TranscriptEntry TypeTables
now render `FormattedContent | undefined` (the alias actually
exported from `chat`), instead of `Root | undefined` which would
force readers to pull the type from `mdast` directly.
- `count` signature uses the new `CountQuery` named type, with a
one-liner describing its single field.
* example(nextjs-chat): fix stale comment on transcripts demo handler
The action handler was relabelled to `transcripts` when it was wired
to `bot.transcripts.list`, but the leading comment still read
"Demonstrate fetchMessages and allMessages" from the previous
iteration. Update it to describe the transcripts demo.
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* feat(slack): use native markdown_text field for outgoing messages
Slack now natively renders markdown via the `markdown_text` parameter on
chat.postMessage / postEphemeral / update / scheduleMessage and via
response_url payloads. The adapter passes markdown through directly instead
of converting to mrkdwn.
- Tables, headings, code fences, blockquotes, and nested lists render
natively in Slack instead of falling back to ASCII / mrkdwn.
- `string` and `{ raw }` messages still go to `text` (preserves literal `*`).
- `{ markdown }` and `{ ast }` messages go to `markdown_text` (12k char limit).
- `renderWithTableBlocks`, `toBlocksWithTable`, `mdastTableToSlackBlock`,
and the AST→mrkdwn renderer (`fromAst` / `nodeToMrkdwn`) are removed.
- `SlackMarkdownConverter` alias is removed; use `SlackFormatConverter`.
- `renderFormatted(ast)` now returns standard markdown (was mrkdwn).
- Incoming `message` events still arrive as mrkdwn and are parsed unchanged.
Net -473 lines across markdown.ts and the five sender call sites.
* fix(slack): use mrkdwn fallback for response_url edits
* docs: cover gchat verification, linear token encryption, shared crypto helpers
The adapter hardening pass in #441 made gchat JWT verification required,
added optional at-rest encryption for Linear OAuth tokens, and promoted
the AES-256-GCM helpers into @chat-adapter/shared. Update the affected
package READMEs and the adapter-authoring guide to match.
* docs(adapter-shared): correct EncryptedTokenData field names
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* fix(slack): enrich link previews with unfurl metadata from attachments
* fix(slack): add trailing slash normalization for unfurl URL matching
* fix(slack): store unfurl metadata from message_changed and enrich subsequent messages
* fix(slack): poll for unfurls so link metadata survives the message_changed race
* [chat] fix Slack streaming team ID for interactive payloads
* chore: downgrade changeset to patch
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* feat(adapter-teams): use native Teams SDK streaming for DMs
Use ctx.stream.emit() from the Teams SDK for DM streaming instead of
manual post+edit. This sends proper typing activities with streamType
channelData, giving the native streaming UI in Teams.
- Capture IStreamer from activity context in handleMessageActivity
- Block handler with deferred promise so stream stays alive during processing
- streamViaEmit() for DMs: uses stream.emit() with incremental text deltas
- Group chats: accumulate full response and post as single message (no flicker)
- Handle StreamCancelledError and stream.canceled for graceful cancellation
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(tests): update Teams streaming assertions for accumulate-and-post
Group chats now accumulate streamed chunks and post as a single message
instead of post+edit, so assertions should check sentActivities not
updatedActivities.
* style: format replay-streaming test
* chore: add changeset for teams native streaming
---------
* docs(adapter-teams): simplify bot setup using Teams CLI
Replace manual 6-step Azure portal walkthrough with Teams CLI commands.
`teams app create` handles AAD registration, secret generation, bot
registration, and channel setup in a single command. Also updates RSC
permission and troubleshooting sections to reference CLI equivalents.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(adapter-teams): remove bot migration section
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(adapter-teams): clarify message history permissions by context
RSC permissions cover channels and group chats (no admin consent).
Azure AD Chat.Read.All is only needed for DM history. Add permission
table and az CLI commands for DM setup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(adapter-teams): simplify bot setup using Teams CLI
Replace manual 6-step Azure portal walkthrough with Teams CLI commands.
Correct message history permissions: RSC for channels/group chats,
Azure AD only for DM history. Add local dev tunnel tip.
* docs(adapter-teams): add teams status step after login
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add chat.getUser() for cross-platform user lookups
Add UserInfo type and optional getUser() method to the Adapter interface.
Implement on Slack (extends existing lookupUser with email/avatar),
Discord, Google Chat, GitHub, Linear, and Telegram adapters.
Add "Who Am I" button to the example app demonstrating the feature.
Update docs with getUser API reference and usage examples.
* fix(chat): improve getUser across slack and gchat adapters
- slack: return null from lookupUser on failure instead of fallback
object, removing the isBot === undefined sentinel in getUser
- slack: use image_192 instead of image_72 for better avatar quality
- gchat: cache avatarUrl from webhook sender payload
- gchat: return avatarUrl in getUser response
- gchat: fix tests to use current cache format with isBot field
- docs: document null return, fix example to use message.author
* chore: fix lint
* docs(chat): include Microsoft Teams in getUser supported adapters list
* feat(adapter-teams): add getUser() support (#404)
* feat(adapter-teams): add getUser() via Microsoft Graph API
- Cache aadObjectId from activity.from during webhook handling
- Implement getUser() using Graph GET /users/{user-id} endpoint
- Requires User.Read.All application permission
- Returns null gracefully when user hasn't interacted or Graph call fails
* docs: add getUser() section to Teams adapter README
* chore: apply ultracite formatting to adapter-teams getUser
* fix(chat): cover all 7 adapters in getUser inference and document per-platform constraints
---------
Co-authored-by: dancer <josh@afterima.ge>