npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@statewavedev/connectors-slack

v0.4.0

Published

Slack connector for Statewave — channel/thread pull + Events-API webhook (messages, reactions, pins, DMs, group DMs) + DM + MPIM (group DM) pull

Downloads

770

Readme

@statewavedev/connectors-slack

Slack connector for Statewave — turns channel and thread activity into normalized episodes under team:<team_id>.

Part of the Statewave Connectors ecosystem.

What it ingests

| Source event | Episode kind | Mode | |---|---|---| | Top-level channel message | slack.message.posted | pull + webhook | | Reply inside a thread | slack.thread.replied | pull + webhook | | Reaction added to a message | slack.reaction.added | webhook (v0.3) | | Reaction removed from a message | slack.reaction.removed | webhook (v0.3) | | Message pinned in a channel | slack.pin.added | webhook (v0.3) | | Message unpinned in a channel | slack.pin.removed | webhook (v0.3) | | Top-level DM message | slack.dm.message.posted | pull (v0.3.1) | | Reply inside a DM thread | slack.dm.thread.replied | pull (v0.3.1) | | Top-level MPIM (group DM) message | slack.mpim.message.posted | pull (v0.3.2) | | Reply inside an MPIM thread | slack.mpim.thread.replied | pull (v0.3.2) |

v0.1 is pull-mode only — it walks conversations.history for each channel you list (and conversations.replies for any threads with replies). Live Events-API mode is on the roadmap.

Quickstart

export SLACK_BOT_TOKEN=xoxb-…
statewave-connectors sync slack \
  --channels general,support \
  --subject team:acme \
  --since 2026-01-01 \
  --dry-run

--channels accepts ids (C0123…) or names (general, #general). At least one is required so you don't accidentally pull a whole workspace on first run. The bot needs channels:history + channels:read (and the groups:* equivalents for private channels you want it to see — invite the bot first).

Options

--channels LIST       comma-separated ids or names (required unless --include-dms)
--include-dms         also ingest DMs the bot has access to (im:read + im:history scopes)
--include-mpim        also ingest multi-party DMs the bot is in (mpim:read + mpim:history scopes)
--subject SUBJECT     override the default `team:<team_id>` subject
--since YYYY-MM-DD    earliest message to consider
--max-items N         cap mapped episodes
--include LIST        allow-list: messages, thread_replies (default: both)
--exclude LIST        deny-list (e.g. --exclude thread_replies for top-level only)
--resolve-users       expand <@Uxxx> mentions to display names (extra API calls per author)
--dry-run             preview mapped episodes without ingesting (recommended for new use)

Direct messages (v0.3.1)

Pass --include-dms to also pull DM history that the bot user has access to. Each DM lands on its own subject — dm:<other_user_id> — so a single sync can mix team:<team_id> channel episodes and per-user DM episodes without colliding. DMs use the slack.dm.message.posted and slack.dm.thread.replied kinds so consumers can route on them separately.

statewave-connectors sync slack --include-dms --since 2026-01-01 --dry-run
statewave-connectors sync slack --channels general,support --include-dms --dry-run

The bot needs the im:read scope (to discover DM conversations) and im:history (to read messages). Bot tokens can only see DMs the bot is itself a participant in — i.e. DMs between a human and the bot user. They cannot read DMs between two humans.

Multi-party DMs / group DMs (v0.3.2)

Pass --include-mpim to also pull multi-party DM history. MPIMs (group DMs in the Slack UI) have no single "other party" — multiple humans share one conversation — so each MPIM lands on its own mpim:<channel_id> subject (Slack's stable channel id). MPIMs emit the separate kinds slack.mpim.message.posted and slack.mpim.thread.replied so consumers can route on them without parsing metadata.

statewave-connectors sync slack --include-mpim --since 2026-01-01 --dry-run
statewave-connectors sync slack --channels general,support --include-dms --include-mpim --dry-run

The bot needs the mpim:read scope (to discover group-DM conversations) and mpim:history (to read messages). Same privacy posture as DMs — group DMs are opt-in because other participants in shared workspaces didn't necessarily consent to having their messages mirrored elsewhere.

Auth

Bot token only (xoxb-…). User tokens, app-level tokens, and OAuth flows are not used in v0.1 — bot tokens are the right default for ingest, since the audit trail in your workspace shows the bot as the reader.

The token is read only from SLACK_BOT_TOKEN and only by this connector. It is never sent anywhere except slack.com/api/*.

Live mode (v0.2)

The same package also ships a Slack Events-API webhook receiver — a pure (Request) => Promise<Response> handler that verifies HMAC signatures, dedups Slack retries by event_id, and ingests every allowed channel message in real time.

Run it as a daemon (zero-config)

export SLACK_SIGNING_SECRET=...           # Slack app → Basic Information → Signing Secret
export STATEWAVE_URL=http://localhost:8100
export STATEWAVE_API_KEY=...              # only if your instance enforces auth

statewave-connectors listen slack \
  --channels C01ABCDEF,C02XYZ123 \
  --port 3000
# → http://0.0.0.0:3000/slack/events

Then expose :3000 to the internet (ngrok / Cloudflare Tunnel / your ingress) and paste the public URL into your Slack app's Event Subscriptions page. Subscribe to message.channels (and message.groups for private channels). The first request Slack sends is a url_verification challenge — the handler echoes it automatically.

Or mount on Vercel / Cloudflare / Express

The handler is framework-agnostic:

// app/api/slack/route.ts (Vercel / Next.js App Router)
import { createSlackWebhookHandler } from '@statewavedev/connectors-slack'

const handler = createSlackWebhookHandler({
  signingSecret: process.env.SLACK_SIGNING_SECRET!,
  channels: ['C01ABCDEF'],
  statewaveUrl: process.env.STATEWAVE_URL!,
  statewaveApiKey: process.env.STATEWAVE_API_KEY,
})

export const POST = (req: Request) => handler(req)
// Cloudflare Workers
import { createSlackWebhookHandler } from '@statewavedev/connectors-slack'

export default {
  async fetch(req: Request, env: Env) {
    const handler = createSlackWebhookHandler({
      signingSecret: env.SLACK_SIGNING_SECRET,
      channels: ['C01ABCDEF'],
      statewaveUrl: env.STATEWAVE_URL,
      statewaveApiKey: env.STATEWAVE_API_KEY,
    })
    return handler(req)
  },
}
// Express (or any Node http server) — adapt with the helper of your choice
import express from 'express'
import { createSlackWebhookHandler } from '@statewavedev/connectors-slack'

const handler = createSlackWebhookHandler({ signingSecret, channels, statewaveUrl })
const app = express()
app.post('/slack/events', express.raw({ type: '*/*' }), async (req, res) => {
  const fetchReq = new Request('http://x/slack/events', {
    method: 'POST',
    headers: req.headers as any,
    body: req.body, // raw bytes — required for signature verification
  })
  const r = await handler(fetchReq)
  res.status(r.status).set(Object.fromEntries(r.headers)).send(await r.text())
})
app.listen(3000)

Cross-process deduplication

The default InMemoryDedupCache is single-process. For multi-replica deployments behind a load balancer, plug in a shared cache:

import { createSlackWebhookHandler, type SlackDedupCache } from '@statewavedev/connectors-slack'

class RedisDedupCache implements SlackDedupCache {
  async seenOrMark(eventId: string): Promise<boolean> {
    // SET key NX EX 600 returns null if it already existed
    const set = await redis.set(`slack:event:${eventId}`, '1', 'NX', 'EX', 600)
    return set === null
  }
}

const handler = createSlackWebhookHandler({
  signingSecret,
  channels,
  statewaveUrl,
  dedupCache: new RedisDedupCache(),
})

Status

v0.4.0 — pull mode (messages + threads + DMs + MPIMs) + Events-API webhook handler (messages, reactions, pins, DMs, group DMs). See RELEASE_NOTES.md.

Subscribing to reactions + pins

In your Slack app's Event Subscriptions → Subscribe to bot events, add (in addition to the message events from v0.2):

  • reaction_added, reaction_removed — needs the reactions:read scope
  • pin_added, pin_removed — needs the pins:read scope

The webhook handler dispatches all four event types automatically; the channel allowlist applies the same way as for messages.

Subscribing to DM + MPIM webhook events (v0.4.0)

The webhook handler also dispatches DM and group-DM messages when the Slack app is configured for them. Two opt-in flags gate the path so a channel-only deployment doesn't accidentally start ingesting DMs the moment someone toggles a Slack-app subscription:

createSlackWebhookHandler({
  signingSecret,
  channels: ['C01ABCDEF'],
  acceptDms: true,    // dispatch message.im events to slack.dm.* on dm:<user>
  acceptMpim: true,   // dispatch message.mpim events to slack.mpim.* on mpim:<channel>
  statewaveUrl,
})

Subscribe to message.im (needs im:history) and message.mpim (needs mpim:history). The handler honors the existing dedup-by-event_id retry guard. DM/MPIM events bypass the channel allowlist because the channel id is a synthetic D… / G… snowflake the operator can't predict.

Out of scope (still planned):

  • Socket Mode (alternative WebSocket transport for the same logical layer)
  • Pull-mode reactions / pinned (would inflate the per-channel API budget; webhook is the right place for these signals)
  • Channel summarization episodes (deferred until LLM-architecture call lands)