@mushi-mushi/plugin-slack-app
v0.2.12
Published
Mushi Mushi plugin for Slack — /mushi slash command router, Slack request verification, OAuth install helpers and an app manifest, for self-hosting. Mushi Cloud has Add to Slack built in.
Maintainers
Readme
@mushi-mushi/plugin-slack-app
Your AI wrote it. Mushi tells you why it broke.
Building blocks for a Mushi Slack app: the /mushi slash command router,
Slack request verification, the OAuth install helpers, and a Slack app
manifest.
Do you need this package? Not on Mushi Cloud: open the Integrations
page of the console and click Add to Slack. Report cards with buttons,
threaded replies and /mushi then work with no code. This package is for
people who run the Mushi backend themselves, or who want to host the Slack
endpoints on their own server.
npm i @mushi-mushi/plugin-slack-appimport { verifySlackRequest, buildSlashRouter } from '@mushi-mushi/plugin-slack-app'Docs: https://kensaur.us/mushi-mushi/docs/plugins/slack
Run standalone, createSlackPlugin(...) posts report and fix events to a
Slack incoming webhook, and its handleSlackInteraction only verifies and
acknowledges a button click; acting on it (dispatching a fix, say) is up to
your server. On Mushi's own backend, clicks are handled by the
slack-interactions function.
One-time Slack app setup (self-hosted Mushi backend)
- Go to api.slack.com/apps → Create from manifest.
- Paste the contents of
manifest.json. It targets the hosted Mushi backend; self-hosters replace thehttps://dxptnwrhwsqckaftyymj.supabase.co/functions/v1prefix with their own functions host:- slash command
/mushi→<functions>/api/v1/webhooks/slack/commands - Events API →
<functions>/api/v1/webhooks/slack/events(Slack sends a one-timeurl_verificationchallenge; the route answers it, so save the manifest with the backend already deployed) - Interactivity →
<functions>/slack-interactions - OAuth redirect →
<functions>/api/v1/webhooks/slack/oauth-callback(Running this package standalone instead? Point everything athttps://<your-host>/slack/command,/slack/interactions(mounthandleSlackInteractionthere; see the note above) and/slack/oauth/callback.)
- slash command
- Copy the signing secret + client ID / secret into the backend secrets
(
SLACK_SIGNING_SECRET,SLACK_CLIENT_ID,SLACK_CLIENT_SECRET). - Install to the workspace from the Mushi console (Integrations page → Slack → Add to Slack) so the bot token is vaulted per project, then invite the bot to the channels where clips will be dropped.
Scopes and events (why each one is in the manifest)
| Scope / event | Used for |
| --- | --- |
| commands | /mushi slash command |
| chat:write, chat:write.public | Report cards, threaded replies, voice transcript cards |
| channels:read | Channel picker in the console |
| channels:history, groups:history, im:history | Receive message events (audio clips arrive as message events with files[]) in public / private channels and DMs |
| files:read | files.info + downloading the clip (url_private_download / aac) for transcription |
| im:read, im:write | Open a DM with the invoking user |
| users:read, users:read.email | Map Slack users to console members |
| message.channels, message.groups, message.im | Voice clips shared in channels / private channels / DMs |
| file_shared | Second signal for a shared clip (deduped on the file id) |
| app_mention, app_home_opened | Existing mention + App Home behaviour |
Voice clips are never transcribed by Slack's own transcription object;
the backend downloads the audio and runs its own speech-to-text, then posts
the verbatim transcript with Confirm / Cancel buttons before anything is
dispatched.
Quick start — Hono server
import { Hono } from 'hono'
import {
verifySlackRequest,
buildSlashRouter,
buildInstallUrl,
exchangeCode,
} from '@mushi-mushi/plugin-slack-app'
const app = new Hono()
const signingSecret = process.env.SLACK_SIGNING_SECRET!
const router = buildSlashRouter({
listReports: async (projectId, limit) => mushi.listReports(projectId, limit),
openReport: async (id) => mushi.getReport(id),
transitionReport: async (id, status) => mushi.transition(id, status),
projectIdForTeam: async (teamId) => db.projectForSlackTeam(teamId),
})
app.post('/slack/command', async (c) => {
const raw = await c.req.text()
const verdict = verifySlackRequest({
signingSecret,
timestampHeader: c.req.header('X-Slack-Request-Timestamp'),
signatureHeader: c.req.header('X-Slack-Signature'),
rawBody: raw,
})
if (!verdict.ok) return c.json({ error: verdict.reason }, 401)
const payload = Object.fromEntries(new URLSearchParams(raw)) as never
return c.json(await router(payload))
})
app.get('/slack/install', (c) => {
const state = crypto.randomUUID()
const url = buildInstallUrl(
{
clientId: process.env.SLACK_CLIENT_ID!,
clientSecret: process.env.SLACK_CLIENT_SECRET!,
redirectUri: process.env.SLACK_REDIRECT_URI!,
scopes: ['commands', 'chat:write', 'users:read'],
},
state,
)
return c.redirect(url)
})/mushi slash command
| Subcommand | Effect |
| -------------------- | ------------------------------------------------------ |
| /mushi voice <text>| Dictate a request (hosted backend only): transcript is read back with Confirm / Cancel buttons before dispatch. |
| /mushi list | Most recent open reports (5 standalone, 10 on the hosted backend). |
| /mushi open <id> | Show classification + summary for one report. |
| /mushi resolve <id>| Transition a report to fixed. |
| /mushi help | Print the command reference. |
All responses are ephemeral (visible only to the invoking user) unless you
override response_type in a custom handler.
Security
verifySlackRequestruns HMAC-SHA256 overv0:<timestamp>:<rawBody>and compares in constant time. Reject requests older than 5 minutes to block replay.- OAuth token rotation is enabled in the manifest — persist both
access_tokenandrefresh_token. - Never log the raw
payloadfrom interactive actions; Slack embeds the invoking user's email there.
License
MIT
Monorepo scale (July 2026): 76 edge functions · 509 SQL migrations · 13 outbound plugins · 11 inbound adapters · 19 pipeline agents. Canonical counts: docs/stats.md · pnpm docs-stats
