Skip to content

feat(webhook): add generic webhook adapter for arbitrary HTTP platforms - #1033

Draft
roboeve[bot] wants to merge 7 commits into
mainfrom
feat/webhook-adapter
Draft

roboeve[bot] wants to merge 7 commits into
mainfrom
feat/webhook-adapter

Conversation

@roboeve

@roboeve roboeve Bot commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds @chat-adapter/webhook, a generic adapter for platforms that have no dedicated adapter but can POST a webhook and accept replies over HTTP. Closes #96.

You describe the platform with two functions, and the adapter handles everything that is the same across HTTP platforms:

const bot = new Chat({
  userName: "mybot",
  adapters: {
    webhook: createWebhookAdapter({
      parse: (event: PlatformEvent) => ({
        id: event.messageId,
        channelId: event.chatId,
        text: event.text,
        author: { userId: event.sender.id },
      }),
      send: async ({ channelId, text }) => {
        await fetch("https://api.example.com/messages", {
          method: "POST",
          body: JSON.stringify({ chatId: channelId, text }),
        });
      },
    }),
  },
  state: createMemoryState(),
});

Design

  • One parse function instead of per-field getters. The issue sketches getText, getSenderId, and getChannelId. A single function that returns { channelId, threadId?, text, author, id?, isMention?, timestamp?, attachments? } covers the same ground, lets one request produce several messages (or none, for receipts and echoes), and keeps related mapping logic in one place.
  • Secure by default. Requests are verified with an HMAC-SHA256 hex digest of the raw body (WEBHOOK_SECRET, x-webhook-signature header, optional sha256= prefix, configurable with signatureHeader). A verify callback replaces this for other schemes. createWebhookAdapter throws when neither a secret nor verify is set, so an endpoint never accepts unauthenticated messages by accident.
  • Optional capabilities are opt-in callbacks. edit, delete, and startTyping enable the matching adapter methods. Without edit, stream buffers and sends once; with it, stream returns null so core uses its post+edit fallback. Missing edit, delete, and reactions throw NotImplementedError.
  • Multiple instances. name (default webhook) is the thread ID prefix and must match the key in adapters, because Chat SDK routes thread IDs by prefix. Thread IDs are {name}:{base64url(channel)}[:{base64url(thread)}], so any platform ID round-trips.
  • History. persistThreadHistory is true, since HTTP platforms rarely expose history; fetchMessages returns nothing and core serves history from the state adapter.
  • Callbacks use method syntax in WebhookAdapterConfig, so parse: (event: MyPayload) => ... type-checks without casts, and send may return nothing.

The adapter is registered like other official adapters: catalog entry (beta: true), docs page at /adapters/official/webhook, create-chat-sdk scaffold spec (placeholder parse/send), root Vitest project, turbo.json env, and a changeset.

Out of scope for this first version: GET verification handshakes (handle them in the route before calling bot.webhooks.<name>), non-JSON bodies (override the protected parseBody), reactions, actions, and DM detection.

Test plan

  • pnpm --filter @chat-adapter/webhook test: 43 tests, including signature verification (valid, missing, wrong secret, tampered body, custom header, unprefixed digest), verify override and throwing verify, 405/400/500 paths, array and empty parse results, outbound send/edit/delete/startTyping, buffered vs. deferred streaming, the shared threadIdContract, and an end-to-end run through a real Chat instance that routes a signed webhook to onNewMention and replies through send.
  • types.test-d.ts: typed parse payloads and void-returning callbacks compile; protected members stay accessible to subclasses.
  • pnpm --filter @chat-adapter/catalog test, pnpm --filter create-chat-sdk test, and pnpm --filter @chat-adapter/integration-tests test (docs/catalog parity, package metadata, README discoverability) pass.
  • pnpm knip, pnpm check, pnpm konsistent, turbo typecheck (all 49 tasks), turbo test --filter='!docs' (all 58 tasks), and turbo build --filter='!docs' pass. The docs app build ran out of memory in my sandbox, so CI is the first full check of the new MDX page.
  • Every code snippet on the new docs page and the generated create-chat-sdk output type-check against the package.

Checklist

  • All commits are signed and verified
  • All commits are signed off for the DCO (git commit -s): the signed commit has no Signed-off-by trailer, so a maintainer may need to add one
  • pnpm validate steps pass, except the docs app build (see test plan)
  • Changeset added
  • Documentation updated

Add @chat-adapter/webhook for platforms without a dedicated adapter. A parse callback maps verified webhook requests to messages and a send callback delivers replies over HTTP, with optional edit, delete, and startTyping callbacks. Requests are verified with an HMAC-SHA256 signature (WEBHOOK_SECRET) or a custom verify function.

Closes #96
@vercel

vercel Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
chat Ready Ready Preview, v0 Oct 19, 2026 10:23am UTC

@vercel-security-reviewer

Copy link
Copy Markdown

Security review details

The custom verify example built its expected credential from process.env.BRIDGE_TOKEN without checking it, so an unset variable produced the guessable value "Bearer undefined" and let forged requests through. Throw at startup when the token is missing and call out the pattern for custom verify functions.
…dleWebhook had already dispatched the earlier ones and then threw, so a platform retry processed them again. Build all messages inside the parse error handler and return 500 before dispatching. Also add the required state adapter to the README example.
@roboeve

roboeve Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

CI failed at pnpm install --frozen-lockfile in every job: merging this branch with current main produced a broken lockfile (no entry for the tsup@8.5.1(...postcss@8.5.28...) peer variant). Reproduced locally by merging origin/main and running a frozen install.

Pushed 858a6f2, which regenerates pnpm-lock.yaml from main, so against main the diff is only the 25-line packages/adapter-webhook importer. Merged with main locally: frozen install passes, the webhook package builds and typechecks, and its 44 tests pass. Waiting on CI for the full run.

…os and catalog so the adapter page header and listing cards show it. Move the WEBHOOK_SECRET note into the info callout used by other quick starts, add a Limitations section, and give the README the OG banner and Environment variables section other adapter READMEs have. Revert the README install line to the adapter package only, matching the other READMEs.

@vercel vercel Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

apps/docs/package.json declares @vercel/geistdocs 2.9.3 and next 16.3.6, which don't match the regenerated pnpm-lock.yaml specifiers (2.11.0 / 16.3.8), breaking pnpm install --frozen-lockfile.

Fix on Vercel

Comment thread packages/catalog/src/adapters/official/webhook.ts Outdated

This branch was successfully deployed

1 active deployment
Preview — a23473aa Deployed Oct 19, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a Generic Webhook / HTTP Adapter for Arbitrary Chat Platforms

0 participants