@crvouga/mockingbird-service-slack

中文文档
3.11.0发布于 2026/11/11MITTypeScriptES Module构建来源已验证近 30 天下载 1.6万

Stateful emulator of Slack incoming webhooks, the Web API, and the Socket Mode control plane, with an outbox of every alert the app sent.

原始使用文档
文档目录7 节
  1. Install
  2. Usage
  3. Routes
  4. Admin (beyond the standard contract)
  5. Namespaces
  6. Deliberately not modelled
  7. API

@crvouga/mockingbird-service-slack

Local emulators. Real API contracts. Part of Mockingbird.

Stateful emulator of Slack for test suites: incoming webhooks, the Web API methods our apps call, and the Socket Mode control plane Bolt needs in order to boot, with an outbox of everything the app "sent". A suite asserts that an alert fired (GET /__admin/outbox?webhook=… or ?channel=…) without a real workspace, and drives the retry paths (429 with retry-after, 5xx) and the terminal ones (no_text, no_service, channel_not_found) with named presets.

  • Operation coverage: SUPPORT.md
  • Slack publishes no maintained OpenAPI for these methods: the contract (openapi.yaml) is hand-authored from Slack's documented wire shapes and our consumers.

Install

npm install -D @crvouga/mockingbird-service-slack

ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with npx mockingbird-slack serve, createServer from ./server (Node), or createRuntime with any Fetch server.

Usage

Point every SLACK_*_WEBHOOK_URL (SLACK_PIPELINE_ALERT_WEBHOOK_URL, SLACK_ERX_ALERTS_WEBHOOK_URL, SLACK_BILLING_ALERT_WEBHOOK_URL, SLACK_CRITICAL_ALERT_WEBHOOK_URL, RECONCILER_SLACK_WEBHOOK_URL, …) at http://127.0.0.1:8808/services/T000/B000/<anything>. Any path is accepted until you register hooks. The Web API callers hardcode https://slack.com/api/…; point them at http://127.0.0.1:8808/api/… (for @slack/web-api, slackApiUrl: "http://127.0.0.1:8808/api/"). Bolt in Socket Mode uses that same clientOptions.slackApiUrl (Bolt copies it into SocketModeReceiver) plus an xapp- app token. apps.connections.open returns a ws:// URL on this process.

npx mockingbird-slack serve --port 8808
import { IncomingWebhook } from "@slack/webhook"
import { WebClient } from "@slack/web-api"
import { createServer } from "@crvouga/mockingbird-service-slack/server"

const server = await createServer()

// The app's own Slack clients. Only the URLs they are given change.
const webhook = new IncomingWebhook(`${server.url}/services/T000/B000/XXXX`)
await webhook.send({ text: "reconcile failed", blocks: [{ type: "divider" }] }) // { text: "ok" }
const slack = new WebClient("xoxb-fixture", { slackApiUrl: `${server.url}/api/` })
await slack.chat.postMessage({ channel: "#alerts", text: "deploy finished" }) // { ok: true, ts, … }

// Test controls. Make the next post fail the way Slack does under load; our clients retry at 0/2/8 s.
server.runtime.applyPreset("rate_limited", "default", { count: 1 })

const outbox = await fetch(`${server.url}/__admin/outbox?webhook=/services/T000/B000/XXXX`)
// { messages: [{ text: "reconcile failed", blocks: [...], ts: "1700000000.000001", thread_ts: null, … }] }
await server.close()

Routes

RouteBehaviour
POST /services/{T}/{B}/{X}Incoming webhook. JSON {text, blocks?, attachments?, thread_ts?} (or form payload=<json>) → 200 ok (text/plain). 400 invalid_payload (not a JSON object), 400 no_text (no text, blocks or attachments), 400 invalid_blocks (structure and Block Kit limits, also inside attachments[].blocks; see below), 400 too_many_attachments, 404 no_service (unregistered hook, once any hook is registered), 404 channel_not_found, 410 channel_is_archived.
POST /api/chat.postMessage{channel, text?, blocks?, attachments?, thread_ts?, unfurl_links?} → {ok, channel, ts, message}. channel is an id or #name; unknown channels are created on first use unless strictChannels. The outbox keeps text, blocks, unfurl_links, thread_ts, and workspace (the team id). Errors: channel_not_found, no_text, invalid_blocks, invalid_blocks_format, msg_too_long, is_archived.
POST /api/chat.update{channel, ts, text?, blocks?} → {ok, channel, ts, text, message} with edited; message_not_found, cant_update_message (webhook posts). Omitted blocks are kept.
POST /api/chat.postEphemeral{channel, user, text} → {ok, message_ts}; user_not_found. Recorded in the outbox with ephemeral: true.
GET|POST /api/chat.getPermalinkchannel, message_ts → {ok, channel, permalink} (https://<domain>.slack.com/archives/C…/p…); message_not_found.
POST /api/reactions.add, /reactions.remove, GET|POST /api/reactions.getPer message (channel, timestamp, name); already_reacted, no_reaction, message_not_found, no_item_specified. reactions.get answers {ok, type: "message", channel, message} with reactions[{name, users, count}].
GET|POST /api/auth.test{ok, url, team, user, team_id, user_id, bot_id} from the workspace settings.
POST /api/conversations.join{ok, channel}; already a member → warning: "already_in_channel"; method_not_supported_for_channel_type (private), is_archived.
GET|POST /api/users.info, /users.lookupByEmail{ok, user} with profile.email/real_name; user_not_found, users_not_found. Seeded: U0ADA ([email protected]) and the bot.
GET|POST /api/files.info{ok, file} with url_private_download on files.slack.com; file_not_found. Seeded: F0REPORT.
POST /api/views.open{trigger_id, view} → {ok, view: {id: "V…", hash, state, …}}; invalid_arguments.
POST /api/apps.connections.openAuthorization: Bearer xapp-… (header only) → {ok, url} where url is ws://<this host>/link/?ticket=…&app_id=…. One WebSocket per ticket; a second upgrade or a closed ticket is rejected. Missing token → not_authed; a bot token → not_allowed_token_type; malformed → invalid_auth; a token in revokedAppTokens → token_revoked; an app token that is not in this namespace's appTokens (when that list is non-empty) → invalid_auth.
WebSocket /link/?ticket=On upgrade, one JSON hello (type, connection_info.app_id, num_connections, debug_info.approximate_connection_time). No events are pushed. Client pings are ponged; the server pings every 2s. {envelope_id} acks are accepted and not answered. disconnect reasons warning, refresh_requested, and link_disabled match Slack's envelope (no reconnect URL: @slack/[email protected] calls apps.connections.open again).

Web API bodies may be JSON or form-encoded (structures JSON-encoded in form fields), as Slack accepts both; read methods also take query arguments. Auth is Authorization: Bearer xox… (or a token form field): missing → not_authed, anything but an xoxb-/xoxp-/xoxa- token (or one outside settings.tokens) → invalid_auth. Logical errors are HTTP 200 {ok: false, error}; a JSON post without charset succeeds with Slack's missing_charset warning.

Admin (beyond the standard contract)

RouteEffect
GET /__admin/outbox?webhook=<path>&channel=<id|#name>&thread_ts=&source=webhook|api&since=&limit=Every message sent, oldest first: text, blocks, attachments, thread_ts, ts, channel, webhook, method, ephemeral, edited, reactions, unfurl_links, workspace. webhook takes /services/T/B/X, T/B/X or a full URL. GET /__admin/outbox/:id reads one.
POST /__admin/hooks{path, channel?} or {hooks: [...]}: register webhooks (and route them to a channel). Once any hook exists, unknown hooks answer 404 no_service. GET lists, DELETE [?path=] removes.
POST /__admin/channels, /users, /filesSeed workspace records ({id?, name, is_private?, is_archived?, is_member?}, {id, name?, real_name?, email?}, {id, name?, mimetype?, size?}). GET /__admin/channels, /users, /views list them.
PUT /__admin/settings{teamId?, teamName?, teamDomain?, botUserId?, botId?, appId?, tokens?, appTokens?, revokedAppTokens?, socketLifetimeMs?, strictChannels?} for the calling namespace. socketLifetimeMs closes an open Socket Mode connection with reason: "warning".
GET /__admin/socket/connectionsOpen and closed Socket Mode sockets for the calling namespace: namespace, appId, ticket, connectedAt, state, reconnectCount. Never tokens.
POST /__admin/socket/disconnect{reason?: "warning"|"refresh_requested"|"link_disabled", code?: 1000}. Sends Slack's disconnect envelope, then closes. code: 1006 drops the TCP connection instead.

Fault presets (POST /__admin/faults {"preset": "<name>", "count"?: n, "params"?: {...}}):

PresetEffect
rate_limitedWebhooks 429 rate_limited, Web API 429 {ok: false, error: "ratelimited"}, both with retry-after (params.retryAfter, default 1 s).
5xx500 internal_error everywhere (params.status: 503 gives service_unavailable).
service_unavailable503 service_unavailable everywhere.
channel_not_foundWebhooks 404 channel_not_found; Web API channel methods {ok: false, error: "channel_not_found"}.
no_serviceWebhooks 404 no_service (a revoked hook).
invalid_authWeb API {ok: false, error: "invalid_auth"} (a revoked token).
connections_open_invalid_authapps.connections.open answers {ok:false, error:"invalid_auth"} and mints no ticket.
connections_open_429apps.connections.open answers 429 {ok:false, error:"ratelimited"} with retry-after (params.retryAfter, default 1).
connections_open_5xxapps.connections.open answers 500 internal_error (params.status: 503 gives service_unavailable).
socket_close_after_helloThe next Socket Mode socket sends hello, then a normal close.
socket_abnormal_closeThe next Socket Mode socket sends hello, then drops the connection (code 1006).
socket_hello_latencyDelay hello by params.latencyMs milliseconds. The HTTP response itself is not delayed.

Namespaces

x-mockingbird-namespace, a /__admin/ns/<name> prefix on the webhook URL or API base, or by credential: PUT /__admin/credentials {"credentials": {"xoxb-worker-a": "a", "T000/B000/XXXX": "b"}} maps a bot token or a webhook's T/B/X path to a namespace.

Deliberately not modelled

  • Inbound Events API and interactivity delivery. Socket Mode's connection lifecycle is modelled (apps.connections.open, hello, ping/pong, disconnect) so Bolt can boot and shut down; the emulator does not push events on connect. A disconnect envelope has no reconnect URL, matching Slack's documented shape and @slack/[email protected], which calls apps.connections.open again.
  • File uploads and downloads: files.info answers seeded metadata; the files.slack.com URLs are not served.
  • Real per-method rate-limit tiers: 429s come only from the rate_limited preset.
  • Channel membership rules for posting (not_in_channel), chat:write.public scopes, and trigger-id expiry for views.open.
  • Message formatting (mrkdwn parsing, link unfurling, mention resolution): text and blocks are stored verbatim.
  • Block Kit limits are a best guess, not oracle-verified (from the per-block pages on docs.slack.dev; the exact status and message Slack returns for each rule was never observed live). blocks and each attachments[].blocks are rejected as invalid_blocks (webhook: 400 invalid_blocks; chat.postMessage, chat.update, chat.postEphemeral: 200 {ok: false, error: "invalid_blocks", response_metadata: {messages}}, the messages being the emulator's own wording) when: there are more than 50 blocks (in each array; whether Slack applies 50 inside attachments is unconfirmed); a block's type is not a Block Kit block type; a section text is over 3000 characters or its fields are over 10 items or 2000 characters each; a header text is not plain_text or is over 150 characters; context has over 10 elements or actions over 25; a block_id is over 255 characters; a text object is empty. Exactly-at-limit values are accepted and stored verbatim, and a rejected message stores nothing (an update leaves the original untouched). Required-field rules (a section with neither text nor fields, an image without alt_text), element-level validation inside actions / context / input / rich_text, and the other attachment fields are not modelled.

API

ExportKindDescription
SlackAPIclassThe in-process emulator: fetch(request), reset(), messages(), state. Options: sqlite, now, namespace, settings.
createRuntimefunctionThe emulator with the full service contract (health, admin, outbox, namespaces, credentials, presets). Options: settings, clock, seed, adminKey, onLog, sqlite.
SLACK_PRESETSobjectEvery named fault preset.
SLACK_NAMESPACEstringThe service name, "slack".
slackCredentialfunctionThe bearer token, or a webhook's T/B/X path (how credentials map to namespaces).
DEFAULT_SETTINGS, DEFAULT_CHANNELS, DEFAULT_USERS, DEFAULT_FILESvaluesThe seeded workspace.
document, operationIds, supportedOperationIdsvaluesThe vendored OpenAPI contract and its operation ids.
createServer, serveTarget, DEFAULT_PORT (./server)NodeServe over node:http; the serve CLI target; port 8808.

Part of mockingbird.

版本与发布记录

@crvouga/mockingbird-service-slack 共发布 39 个版本,首次发布于 2026/09/23;过去 12 个月发布了 39 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
3.11.0latest2026/11/11今天
3.10.62026/11/038 天前
3.10.52026/11/038 天前
3.10.42026/11/038 天前
3.10.32026/11/038 天前
3.10.22026/11/038 天前

关于 @crvouga/mockingbird-service-slack 的常见问题

@crvouga/mockingbird-service-slack 是什么?

Stateful emulator of Slack incoming webhooks, the Web API, and the Socket Mode control plane, with an outbox of every alert the app sent.

如何安装 @crvouga/mockingbird-service-slack?

在项目目录运行 npm install @crvouga/mockingbird-service-slack,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 mockingbird-slack,可以用 npx mockingbird-slack 直接运行。

@crvouga/mockingbird-service-slack 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@crvouga/mockingbird-service-slack 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@crvouga/mockingbird-service-slack 可以商用吗?

@crvouga/mockingbird-service-slack 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@crvouga/mockingbird-service-slack 的下载量有多少?

根据 npm 官方下载统计,@crvouga/mockingbird-service-slack 在 2026/10/11 – 2026/11/09 这 30 天内被下载 15,543 次,日均约 518 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@crvouga/mockingbird-service-slack 还在维护吗?

最新版本 3.11.0 发布于 2026/11/11(今天);至今共发布 39 个版本,其中过去 12 个月发布了 39 个;首次发布于 2026/09/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。