Skip to content

feat(slack): expose direct WebClient access via adapter.client#471

Merged
bensabic merged 6 commits into
mainfrom
feat/slack-direct-webclient-access
May 9, 2026
Merged

feat(slack): expose direct WebClient access via adapter.client#471
bensabic merged 6 commits into
mainfrom
feat/slack-direct-webclient-access

Conversation

@bensabic
Copy link
Copy Markdown
Contributor

@bensabic bensabic commented May 9, 2026

Mirrors the Linear and GitHub adapter pattern by exposing the underlying @slack/web-api WebClient as adapter.client. Use it for any Slack Web API call not covered by the SDK's high-level methods, e.g. bot.getAdapter("slack").client.pins.add(...).

Token resolution order:

  1. The token from the current request context — set automatically during webhook handling, or by adapter.withBotToken(token, fn).
  2. The default botToken when configured as a static string or a synchronous resolver function.

Throws AuthenticationError outside any context in multi-workspace mode, or when botToken is configured as an async resolver. In both cases, await the token first and bind it explicitly with adapter.withBotToken(token, () => adapter.client...).

Internally the existing private client: WebClient 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.

What's changed

Slack adapter (@chat-adapter/slack)

  • New public adapter.client getter returning a token-bound WebClient.
  • Per-token WebClient cache so repeated access reuses instances; distinct workspaces get distinct clients.
  • apiUrl is now stored as a private field and threaded into the bound WebClient, including SLACK_API_URL env var resolution.
  • Fix: createSlackAdapter() no longer silently drops the apiUrl config field — it's now passed through to the SlackAdapter constructor.
  • Minor changeset.

Integration tests (@chat-adapter/integration-tests)

  • injectMockSlackClient updated to write to the renamed internal _client field.

Documentation

  • apps/docs/content/docs/api/chat.mdx — Slack added to "Direct client access" section, code example, type table row, and an updated Callout that spells out both multi-workspace and async-resolver constraints.
  • apps/docs/content/docs/usage.mdx — Slack added to the typed-.client quick reference.
  • packages/adapter-slack/README.md — new "Direct WebClient access" section with usage example, token resolution order, and the withBotToken() workaround for async resolvers.

Example app (examples/nextjs-chat)

  • "Channel Info (Slack)" button: calls slack.client.conversations.info (uses existing channels:read scope) and renders the result as a two-column <Table>.
  • "Pin Message (Slack)" button: calls slack.client.pins.add on the welcome card itself (event.messageId).
  • slack-manifest.yml adds pins:write for the new Pin button.

Tests

  • 11 new tests in packages/adapter-slack/src/index.test.ts covering: static-token binding, sync resolver, withBotToken override of default, apiUrl propagation through the factory, SLACK_API_URL env var resolution, distinct-tokens-distinct-clients caching, multi-workspace outside-context throw, async-resolver throw, and end-to-end token routing through event.adapter.client.token inside a real block_actions webhook dispatch.

bensabic added 6 commits May 9, 2026 16:47
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.
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.
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.
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.
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).
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.
@bensabic bensabic requested a review from a team as a code owner May 9, 2026 07:10
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 9, 2026

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

Project Deployment Actions Updated (UTC)
chat-sdk-nextjs-chat Ready Ready Preview, Comment May 9, 2026 7:10am

@bensabic bensabic merged commit 8366b8b into main May 9, 2026
13 checks passed
@bensabic bensabic deleted the feat/slack-direct-webclient-access branch May 9, 2026 08:59
visyat added a commit that referenced this pull request May 9, 2026
…t" (#472)

* Revert "feat(slack): expose direct WebClient access via adapter.client (#471)"

This reverts commit 8366b8b.

* 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>
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.

2 participants