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

@volter/twin-slack

v2.0.0

Published

Local Slack twin — Web API + Events API; your real `@slack/web-api` talks to it unmodified. Mirror, simulate, and fork. Built on @volter/world-core.

Readme

@volter/twin-slack

The Slack twin: a local replica of Slack's method-style Web API (POST /api/<method> → { ok, … }), derived from Slack's published spec (248 methods, 227 served by the pack's semantics). It covers a workspace's life across Slack's plans: messaging, files, reactions, pins, saved items, reminders, do-not-disturb, user groups, calls, canvases, views and workflows; apps (manifests, OAuth installs, app-level tokens and Socket Mode, the App Directory's install requests); and, on Enterprise Grid, the Admin API and SCIM that an org admin or an identity provider calls. What only Slack's pages do (an app's settings, "Invite people" and install requests, the OAuth consent) the pack serves as screens. Web API refusals are Slack's HTTP 200 { ok: false, error }; incoming webhooks answer instead with HTTP statuses and plain-text bodies (ok, invalid_payload, channel_is_archived, …). The real @slack/web-api WebClient works against it unmodified via slackApiUrl.

Surface

  • Web API (slack-twin.ts; HTTP wrapper slack-server.ts → createSlackTwinServer): conversations.list/info/history, users.list/info, auth.test.
  • Writes (applySlackWrite): chat.postMessage, chat.update, conversations.create — entries in the shared kernel tree; the --read-only flag returns ok:false.
  • OAuth (screens/oauth.tsx, semantics/apps.ts): an install or a Sign in with Slack names an app made with apps.manifest.create (a configuration token from api.slack.com/apps), and the consent shows the app's display_information.name. A client_id with no app is refused on Slack's error page ("Something went wrong when authorizing this app", "Error details: Invalid client_id parameter"), and oauth.v2.access / openid.connect.token answer invalid_client_id, or bad_client_secret for a secret not the app's. Register the app first, as an operator does. The World's own app is the exception: when the World's env sets SLACK_CLIENT_ID (with SLACK_CLIENT_SECRET and SLACK_SIGNING_SECRET; init mints them), the twin takes it as an app already made, read only through worldEnvValue, never from the caller's shell. A client id with no secret is no app. Its name is the World's name (VOLTER_WORLD_NAME, the instance name, world.json's id by default); it declares no Redirect URLs, so it goes back to the one each request names (a request naming none is refused), and it declares no slash commands or Request URLs; consent, oauth.v2.access, openid.connect.token and apps.uninstall take its client id and secret. An app made with the same client id takes precedence. The workspace client's deliveries to a made app's Request URL (/_twin/client/*) carry X-Slack-Request-Timestamp and X-Slack-Signature signed with its signing secret; the timestamp is the World clock's, so a verifier that also rejects future timestamps refuses deliveries from a clock set away from now. An app another company made in its own workspace and publicly distributed is stated through POST /_twin/apps {workspace, manifest} (the World holds only its customer's workspace), which answers apps.manifest.create's credentials.
  • Incoming webhooks (slack-webhooks.ts): an install asking for incoming-webhook has the installer pick a channel on the consent (a public one, or a private one they are in; with none, the consent offers only Cancel), and oauth.v2.access answers incoming_webhook { channel, channel_id, configuration_url, url }. A POST to the hooks.slack.com/services/… url posts as the install's bot into that channel: 200 ok, or 400 no_text / 400 invalid_payload, 404 no_service, 404 channel_not_found, 410 channel_is_archived. A picked channel that is gone refuses the exchange instead of answering without the webhook, and an org-level install asking for incoming-webhook is refused at the consent.
  • Events (slack-events.ts): Events-API event_callback envelopes on write (message, channel_created).
  • Conformance (slack-conformance.ts): field-name subset vs the vendored Slack channel/user/message object schemas (standing gate; also strips connector bookkeeping like createdAt so served objects are pure Slack shapes).
  • UI mirror (slack-mirror-ui.ts): a Slack-style workspace (React), a pure frontend (R3) — the shell + assets with the twin's own fetch adapter mounted beside them. It reads the workspace through the twin's store door GET /twin/store/mirror (the one named projection slack-mirror-state.ts builds server-side, through the twin's own served reads) and writes through the Web API (POST /api/chat.postMessage). --wire hides the doors, so the client renders from Web API reads alone, as it must against a real workspace. Pure helpers both sides share live in slack-shared.ts.

CLI

world-slack serve [--read-only] [--port N] [--root DIR]
world-slack mirror [--wire] [--port N] [--host ADDR] [--root DIR]
world-slack conformance [--root DIR]
world-slack shadow --channel C…[,C…] [--loop SECONDS] [--limit N] [--root DIR]

shadow mirrors real channels into the local twin pull-only (needs SLACK_TOKEN/ SLACK_USER_TOKEN/SLACK_BOT_TOKEN): incremental via durable per-channel cursors under --root, file bytes included, one JSON report per tick. Each tick drains every cursor-paginated history page newer than the durable high-water mark before advancing it; provider errors and failed file downloads abort the tick without acknowledging those messages, so a later tick can retry them instead of silently losing data. It imports no push path — local writes stage as pending actions and are counted in the report, never applied — and no SDK: the pack's own makeSlackReadClient(token) covers the pull surface. Programmatic form: slackShadowTick(client, { channels, root, occurredAt }).

Point the real @slack/web-api WebClient at it via slackApiUrl (http://127.0.0.1:<port>/api/).

SLACKAPP_TWIN_URL optionally routes slack.com/api/apps.connections.open through a separate app-token credential boundary. It takes precedence for that method; all other Web API calls use SLACK_TWIN_URL. With only SLACK_TWIN_URL, routing is unchanged. World initialization points both variables at the same Slack service. The standalone API and mirror servers support Socket Mode for the synthetic app A-twin: use an xapp-* app token with the real Slack Socket Mode client. apps.connections.open returns a one-use, one-minute URL on the serving origin (ws locally, wss over HTTPS). Connections receive hello and events_api envelopes from successful Web API writes; thread, bot, mention, edit and deletion fields come from the same stored messages. Acknowledge envelope_id; unacknowledged envelopes have up to two retries at three-second intervals. Up to ten connections share delivery; each event goes to one connection, not all. Transport state is per server, cleared on stop; offline/restart replay is not supplied.

A fetch-only host must mount SlackSocketMode.upgrade through its WebSocket server and pass that same instance as socketMode to createSlackTwinFetch. Without a transport, apps.connections.open returns not_supported, never a dead URL. Socket Mode interactive payloads, configurable app subscriptions, and RTM typing/presence remain open coverage.

Interaction surfaces

  1. SDK/API — zero edits (preferred): SLACK_TWIN_URL=http://127.0.0.1:PORT node --require @volter/world-core/inject your-app redirects the real @slack/web-api from slack.com to the twin. Or override directly: new WebClient(token, { slackApiUrl: 'http://127.0.0.1:PORT/api/' }).
  2. API + CLI — run the twin in a World and inspect it with volter world log and diff. Use the deployment guide for changeset review and deployment.
  3. Read-only — world-slack serve --read-only: unlimited local reads, no rate limits; writes refuse like Slack ({ ok: false }).
  4. UI mirror — world-slack mirror renders a Slack-style workspace over the twin's state.

See getting started and zero-edit injection.

Changesets and deployment

The deployment guide owns the shared changeset, verify, approve, push and deploy workflow. Push moves entries between Worlds; the root's policy controls vendor execution. Consult this package's declared capabilities and protocol standing for supported Slack operations; do not use removed plan/review helpers.

Coverage

Goal: honest, explicitly tracked coverage of Slack's core feature surface. Every capability is either done or a tracked todo; anything not done is a gap to close.

Done — conversations: list/info/history/replies/members + create/join/leave/invite (membership tracked, num_members/is_member updated); chat: postMessage/update/delete/ getPermalink + scheduleMessage/deleteScheduledMessage/scheduledMessages.list; threads (parent/reply split, reply_count/reply_users/latest_reply derived); reactions add/ remove/get; pins add/list (pin_count, pinned_to); bookmarks add/list; users list/info + profile.get/set (incl. custom status), getPresence + setPresence, dnd.info + setSnooze/endSnooze; files info/list/delete (+ derived shares/channels); team.info; views open/update/push/publish (modals + App Home, hash optimistic-concurrency); Canvas create/edit/delete + sections.lookup + conversations.canvases.create (markdown body parsed into addressable sections; channel canvas stamps the real properties.canvas channel field; canvas state held in twin-internal canvas subjects, never in slackState); Block Kit blocks + message metadata round-trip (stored verbatim, returned on history); opaque cursor pagination + oldest/latest/inclusive time windows; faithful { ok, ... } shapes with real Slack error codes; Events-API emission (event_callback on write: message/channel_created/reaction/pin/member/file); interactivity dispatch — delivers vendor-faithful block_actions / view_submission / view_closed payloads (deterministic trigger_id from occurredAt+seq; modal payloads embed the REAL stored view + merged state.values) and slash-command POSTs (command/text/user_id/channel_id/ response_url/trigger_id) to registered endpoints via injected delivery (fake in tests, HTTP POST live — no real network), incl. the ack/response_action (clear/update/push/errors) shape; R2 conformance + recorded-diff gates; UI mirror (rung-5 structural DOM checklist ✅, incl. a huddle tray for the Calls API); connector pull (channel + history, incremental cursor) + push (postMessage/update/delete/scheduleMessage/deleteScheduledMessage, reactions, conversations create/join/leave/invite, pins.add, files.delete, bookmarks.add, profile.set, setPresence, dnd snooze/end — confirmed per action); Calls API (calls.add/update/end/info + participants.add/remove — used to model huddles as in-channel live calls, rendered in the huddle tray); Workflow Builder steps (workflows.stepCompleted/Failed/updateStep) + custom functions (functions.completeSuccess/Error)

  • workflow triggers (workflows.triggers.create/list/update/delete — link/shortcut/event/ scheduled/webhook records, with minted webhook/shortcut urls); legacy secondary message attachments (chat.postMessage/update — normalized 1-based id, round-trip on history); file comments (files.comments.add/list/delete — bumps comments_count); files.remote.* (add/info/update/remove/list — external file refs as mode:external file objects, addressable by file id or external_id); file BYTES (files.upload's content param and the full v2 flow — getUploadURLExternal → POST bytes to upload_url → completeUploadExternal — store real content content-addressed under this service's own world-state dir (slack-blobs.ts — a PACK-LOCAL key space over the kernel's BlobStore seam (runtime contract R11), so a local world's world scrub deletes it like every other pulled chat artifact while a hosted namespace serves the same keys out of object storage); url_private/url_private_download serve the exact bytes back with bearer auth, API responses rewrite twin-domain URLs to the live server origin, the connector push replays uploads via files.uploadV2 with the stored bytes (as the SAME file.upload action — no separate pending companion), and pull takes an injected fetchFileBytes downloader, folded through the shared observation path used for mutable resources in this pack, to mirror inbound file content without orphaning bytes that arrive after metadata — failed content downloads refuse cursor advancement so the entire message is retryable; makeUrlPrivateFetcher(token) is the exported default, and shadow mode (world-slack shadow / slackShadowTick) wraps sync into a pull-only mirror loop with durable cursors that advance only after all newer history pages are drained); world-clock stamping on write methods (every write is stamped with the world's clock — deterministic history is clock-set → seed → clock-advance); Enterprise Grid admin.* — admin.users (invite/remove/role changes + list), admin.conversations (archive/unarchive/delete/rename/convertToPrivate/setTeams + search + getConversationPrefs), admin.teams (create/settings + list), admin.apps (approve/restrict + approved/restricted lists); SCIM provisioning (Users create/list/delete, Groups create/list — mapped to chat users/subteams); Audit Logs API (audit.v1.logs/actions/schemas — admin.* role/channel/app/workspace actions append faithful audit entries, read back newest-first + action-filtered); Slack Connect (conversations.inviteShared + externalInvitePermissions.set — stamps the real is_pending_ext_shared/pending_shared channel fields); canvas sharing (canvases.access.set/delete); Sign in with Slack (users.identity); team.accessLogs/billableInfo/integrationLogs; app lifecycle (apps.connections.open negotiation url + apps.uninstall).

Planned (known-missing, will do) — scheduled DND windows (dnd.setSnooze models the manual snooze only, so next_dnd_start_ts/next_dnd_end_ts are not real schedule bounds); search.messages deepening (substring matching over stored text, not Slack's query grammar); no live dispatch of Block Kit view submissions (views are stored for read-back, not driven by an interaction loop).

Planned (todo) —

  • Socket Mode / RTM websockets: serve the persistent transport and push events over it (the HTTP Web API + Events API surface is fully modeled).
  • admin.analytics.getFile bytes and the unsupportedVersions export zip: produce the dump and the report from the workspace/session state the twin holds (the envelopes are modeled).
  • Legacy binary multipart on files.upload: the deprecated file= multipart field is not parsed (Slack itself deprecated this path in favor of the v2 flow, which the twin models WITH bytes). Uploads that arrive with neither content nor the v2 flow keep a real-shaped file object with declared size and no stored content — and the connector still skips replaying those loudly rather than pushing a hollow file.
  • Ephemeral typing indicators ride along with the RTM transport above; avatar/team-icon URLs are synthetic rather than stored image bytes.

Rate budget — the fail-closed backstop on live calls

makeSlackReadClient is the one place this pack issues a live request, so every call it makes is charged against a persistent, fail-closed spend ledger before the request goes out. Past the ceiling, or while a Retry-After/429 cooldown is armed, it throws instead of calling. The ledger is keyed by vendor and a hash of the credential (limits are per credential, so it is deliberately not cwd-scoped) and persists across processes, so a fresh process does not get a fresh allowance; a corrupt ledger counts as a full window rather than zero spend. There is no option to disable it, and no value you can pass for budget that yields an unguarded client — an injected budget is validated by method identity, so a subclass or a Proxy that replaces checkBudget is refused.

The declared numbers: 60 weighted units / 60s at weight 2 = 30 calls/minute — exactly the kernel's austere fallback, because Slack's tightest published tier is 1+ per minute. conversations.history/.replies cost 12 (5 a minute, a tenth of their nominal Tier 3). ⚠️ Slack cut those two methods to 1 request/minute for non-Marketplace apps created after 2025-05-29; this ceiling does not model that — it is sized for the Tier 3 a Marketplace-approved or internal app still gets, and for such an app the vendor's 429 arrives first and is converted into a persisted stop. ⚠️ This guards the client the pack builds; the push/sync entrypoints take an injected SlackClientLike whose calls do not pass through it.

The mechanism is shared and vendor-agnostic — it lives in the kernel (@volter/world-core → packages/world-core/src/rateBudget.ts); what lives here in src/slack-budget.ts is this vendor's declaration (window, ceiling, per-endpoint weights, and a reason citing the limits above) plus the vendor-bound SlackBudget. The rule is ratified as ../../../docs/contributing/architecture.md D8, and the kernel module's header documents what the guard does not guarantee — read that before trusting it.