feat(slack): App Home tab — integrations list + per-user Connect/Disconnect#653
Conversation
The App Home tab now shows each integration's per-user OAuth status (Connected/Disconnect vs. Connect). Connect runs the MCP auth-code flow and re-publishes the tab with an 'Open sign-in' link; Disconnect revokes the stored credential. Adds getMcpConfigService to CoreServices and exports deleteCredential from the device-auth route module.
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d891940dd8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "vite": "^6.0.0", | ||
| "vitest": "^2.1.8", | ||
| }, | ||
| }, | ||
| }, | ||
| "packages": { |
There was a problem hiding this comment.
Restore the packages/web workspace lock entries
This lockfile was generated without the packages/web submodule initialized: the commit removes the packages/web importer and hundreds of its resolved packages even though the root package.json still declares packages/* workspaces and packages/web is a tracked submodule. In any checkout/CI job that initializes the web submodule, a frozen root bun install will see a workspace that is missing from the lockfile (or rewrite the lock), and web builds/dev installs lose the pinned dependency graph. Please regenerate the lockfile with packages/web present or revert this unrelated lockfile pruning.
Useful? React with 👍 / 👎.
What
Rebuilds the Slack App Home tab (the old
views.publishcontent — "AI coding assistant", "Open Settings" — was deleted in the Chat-SDK migration and never re-published; Slack was serving a stale cached view). The new tab:✅ Name · connected+ a Disconnect button (revokes the stored credential viadeleteCredential)⚪ Name · not connected+ a Connect button → runs the MCP auth-code OAuth flow (startAuthCodeFlow, well-known discovery, PKCE, per-user scope key) and re-publishes the tab with an Open sign-in ↗ link for that row⚪ Name(no button) for integrations that don't require sign-inlobu-memory) hiddenlobu run//lobu link <code>instructions instead; the integrations section is skipped (no MCP-config access for placeholder agents).How it's wired
registerSlackAppHome(chat, connection, { mcpConfigService, secretStore, publicGatewayUrl })inslack-platform-bridge.ts— subscribes tochat.onAppHomeOpenedandchat.onAction([connect, disconnect])(first-class Chat-SDK hooks; the Slack adapter already routesblock_actionswithcontainer.type === "view"toprocessAction).getStoredCredentialagainst the same credential store the MCP proxy writes (mcp-auth/{agentId}/{userId}/{mcpId}/credential).getMcpConfigService()added to theCoreServicesinterface (the concrete impl already had it).deleteCredentialexported fromroutes/internal/device-auth.ts(previously only reachable via the worker-authenticatedDELETE /internal/device-auth/credentialroute).Security
event.user.userId(from Slack's signedblock_actionspayload) as the credential scope key; the agent id is fixed by the connection, not the payload.POST /api/v1/webhooks/:connectionId, which verifies the connection's signing secret before dispatch.mcpIdvalidated — the only attacker-influenced field (value) is checked againstgetMcpStatus(agentId)before anysecretStore.delete/ OAuth start; unknown ids are a logged no-op.OAuthStateStore(5-min TTL, atomic single-use), server-controlled redirect URI,userIdbaked into the stored state; the authorization link is published only to the user who clicked (views.publishis per-user_id).Known limitations (not gaps, but worth noting)
.well-knowndiscovery (wwwAuthenticate: null); MCP servers that advertise OAuth metadata only via theWWW-Authenticateheader on a 401 won't be discoverable from the Home tab (the in-thread flow gets that header for free by reacting to a real 401). Such a Connect click fails closed (logged, tab unchanged). Pre-existing limitation ofstartAuthCodeFlow.auth_scope = "channel") are shown with a per-user scope key, which won't reflect a shared channel credential. Rare opt-in; not special-cased.Tests
packages/server/src/gateway/__tests__/slack-platform-bridge.test.ts— 9 tests: status badges (connect vs disconnect button), disconnect → revoke + re-publish, disconnect rejects a crafted unknownmcpId, connect → mints URL + re-publish (mocked flow), no-deps fallback, preview workspace.make build-packages,bun run typecheck, fullsrc/gatewaysuite green (the one pre-existingplatform-file-handler.test.tsfailure also fails on cleanmain— unrelated).🤖 Generated with Claude Code