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

@sedibyte/dispatch

v0.5.0

Published

Provider-agnostic dispatcher for taking actions against SaaS platforms (send Teams messages, emails, subscribe to Salesforce record changes, and more to come).

Readme

@sedibyte/dispatch

A provider-agnostic dispatcher for taking actions against SaaS platforms — send a Teams message, an email, upsert a record — behind one small, consistent surface:

await dispatch.teams.send({ chatId: "19:[email protected]", text: "Deploy finished ✅" });

The core owns one cross-cutting concern — composing providers into a single, strongly-typed object — so each SaaS integration only has to know how to perform its own actions. Adding a new provider doesn't touch the core.

Authentication is not reimplemented here: providers acquire tokens through @sedibyte/auth, which owns the provider registry and token caching.

Written in TypeScript; ships compiled ESM plus type declarations.

Requirements

  • Node.js >= 18 (uses the built-in fetch)
  • ESM ("type": "module")
  • @sedibyte/auth as a peer dependency (you install it alongside this package)

Install

npm install @sedibyte/dispatch @sedibyte/auth

Usage

Compose the providers you need, then call their actions. Everything is configuration — no per-customer code.

import { createDispatch } from "@sedibyte/dispatch";
import { teams } from "@sedibyte/dispatch/teams";

const dispatch = createDispatch(
  teams({
    credentials: {
      tenantId: process.env.MS_TENANT_ID!,
      clientId: process.env.MS_CLIENT_ID!,
      clientSecret: process.env.MS_CLIENT_SECRET!,
    },
  }),
);

// Post into a chat…
await dispatch.teams.send({ chatId: "19:[email protected]", text: "Hello 👋" });

// …or a channel…
await dispatch.teams.send({
  teamId: "<team-id>",
  channelId: "19:[email protected]",
  html: "<b>Build 42</b> shipped",
});

// …or notify a user via their activity feed.
await dispatch.teams.send({
  user: "[email protected]",
  text: "Build 42 shipped",
  activityType: "deploymentComplete", // must exist in your Teams app manifest
});

Discord works the same way — send to a channel or a user, and discover where you can post:

import { discord } from "@sedibyte/dispatch/discord";

const dispatch = createDispatch(
  discord({ botToken: process.env.DISCORD_BOT_TOKEN! }),
);

// Post into a channel…
await dispatch.discord.send({ channelId: "123", content: "Deploy finished ✅" });

// …or DM a user…
await dispatch.discord.send({ user: "456", content: "Build 42 shipped" });

// …and list the servers/channels the bot can reach.
const guilds = await dispatch.discord.listGuilds();
const channels = await dispatch.discord.listChannels({ guildId: guilds[0]!.id });

Exchange sends email through Microsoft Graph, sharing the same app-only auth as Teams:

import { exchange } from "@sedibyte/dispatch/exchange";

const dispatch = createDispatch(
  exchange({
    credentials: {
      tenantId: process.env.MS_TENANT_ID!,
      clientId: process.env.MS_CLIENT_ID!,
      clientSecret: process.env.MS_CLIENT_SECRET!,
    },
    from: "[email protected]", // default sender mailbox
  }),
);

// Send an email…
await dispatch.exchange.email({
  to: "[email protected]",
  subject: "Deploy finished",
  html: "<b>Build 42</b> shipped ✅",
});

// …with cc/bcc, a per-message sender, and an attachment.
await dispatch.exchange.email({
  from: "[email protected]",
  to: ["[email protected]", { address: "[email protected]", name: "Team Lead" }],
  cc: "[email protected]",
  subject: "Nightly report",
  text: "See attached.",
  attachments: [
    { name: "report.pdf", contentType: "application/pdf", content: pdfBase64 },
  ],
});

As more providers land, the same object grows the pattern you already know:

await dispatch.teams.send({ ... });
await dispatch.discord.send({ ... });
await dispatch.exchange.email({ to: "...", subject: "...", html: "..." });
await dispatch.slack.send({ channel: "#alerts", text: "..." });
await dispatch.salesforce.upsert("Memo__c", { ... });
await dispatch.github.listCommits({ owner: "sedibyte", repo: "dispatch" });
await dispatch.jira.search({ jql: "project = PROJ ORDER BY created DESC" });

Every argument is passed in explicitly — nothing is read from the environment. The caller owns loading credentials (from env, a vault, wherever).

Providers

Teams — @sedibyte/dispatch/teams

teams(options) returns a provider for createDispatch. Choose exactly one way to authenticate:

| Option | When to use | | --- | --- | | credentials: { tenantId, clientId, clientSecret } | The common case. Registers a Microsoft app-only provider with @sedibyte/auth and uses it. | | authProviderName: string | Reuse a Microsoft provider you already registered with @sedibyte/auth (e.g. a named tenant). | | graphClient: GraphClient | Inject an existing Graph client (advanced / testing). |

Extra options: namespace (key on the dispatch object, default "teams"), defaultActivityType (fallback for user notifications), and graphBase (override the Graph endpoint, e.g. a sovereign cloud).

dispatch.teams.send(options) picks its target from the options:

  • Chat message{ chatId, text | html, subject?, importance? }
  • Channel message{ teamId, channelId, text | html, subject?, importance? }
  • User notification{ user, text, activityType?, topic?, webUrl?, templateParameters? }

It returns { kind: "chat" | "channel" | "activity", id? }.

Required Microsoft Graph permissions

Grant your app registration (admin consent) the permissions for the actions you use. These are application permissions, since Dispatch authenticates app-only through @sedibyte/auth:

  • Chat messages: ChatMessage.Send (+ Chat.ReadWrite.All if creating chats)
  • Channel messages: ChannelMessage.Send
  • User activity notifications: TeamsActivity.Send

User notifications also require the activityType to be declared in your Teams app manifest. See Microsoft's docs on sending activity feed notifications.

Discord — @sedibyte/dispatch/discord

discord(options) returns a provider for createDispatch. Authentication is delegated to @sedibyte/auth, which supports both Discord's static bot tokens (Authorization: Bot <token>) and its OAuth2 application grants. Choose exactly one way to authenticate:

| Option | When to use | | --- | --- | | botToken: string | The common case. Registers a bot-token provider with @sedibyte/auth and uses it. | | credentials: DiscordCredentials | Authenticate as a Discord application via OAuth2 — the client-credentials grant ({ clientId, clientSecret }), or the refresh-token grant ({ clientId, clientSecret, grantType: "refresh_token", refreshToken }). | | authProviderName: string | Reuse a Discord provider you already registered with @sedibyte/auth. | | client: DiscordClient | Inject an existing @sedibyte/auth Discord client (advanced / testing). |

botToken and credentials may also take an authProviderName to register under a specific key. Extra options: namespace (key on the dispatch object, default "discord"), and apiBase (override the Discord API base URL, e.g. to pin an API version). Credentials are never read from the environment — the caller passes them in.

dispatch.discord exposes four actions:

  • Send a channel messagesend({ channelId, content?, embeds?, tts?, allowedMentions? })
  • Send a direct messagesend({ user, content?, embeds?, tts?, allowedMentions? }) (the client opens the DM channel for you)
  • List serverslistGuilds()DiscordGuild[] (transparently pages through every guild the bot is in)
  • List channelslistChannels({ guildId })DiscordChannel[]

send requires content or at least one embed, and returns { kind: "channel" | "dm", id, channelId }.

Required Discord setup

Create a bot application in the Discord Developer Portal and use its bot token. The bot must be invited to a server before it can list or post there. Relevant gateway/permission notes:

  • Sending messages needs the bot to have Send Messages (and View Channel) permission in the target channel.
  • DMing a user generally requires a shared server with that user.
  • listGuilds() returns the servers the bot itself has been added to (GET /users/@me/guilds).

Exchange — @sedibyte/dispatch/exchange

exchange(options) returns a provider for createDispatch. It shares Teams' Microsoft app-only authentication (delegated to @sedibyte/auth); choose exactly one way to authenticate:

| Option | When to use | | --- | --- | | credentials: { tenantId, clientId, clientSecret } | The common case. Registers a Microsoft app-only provider with @sedibyte/auth and uses it. | | authProviderName: string | Reuse a Microsoft provider you already registered with @sedibyte/auth (e.g. a named tenant). | | graphClient: GraphClient | Inject an existing Graph client (advanced / testing). |

Extra options: namespace (key on the dispatch object, default "exchange"), from (default sender mailbox, used when a call omits its own from), and graphBase (override the Graph endpoint, e.g. a sovereign cloud).

dispatch.exchange.email(options) sends a message through the sender mailbox's sendMail endpoint:

  • Recipientsto, and optional cc / bcc / replyTo. Each accepts a single value or an array; a value is either a bare address ("[email protected]") or { address, name? }.
  • Senderfrom (a mailbox id or userPrincipalName). Required, either per-call or via the provider's default from.
  • Contentsubject, plus text or html (html wins when both given).
  • Extrasimportance ("low" | "normal" | "high"), saveToSentItems, and attachments ({ name, content /* base64 */, contentType?, inline?, contentId? }).

It returns { kind: "email", from }.

Required Microsoft Graph permissions

Grant your app registration (admin consent) the application permission Mail.Send, since Dispatch authenticates app-only through @sedibyte/auth. Note that Mail.Send lets the app send as any mailbox in the tenant; scope this down with an application access policy if you need to restrict which mailboxes it can send from. See Microsoft's docs on sending mail.

GitHub — @sedibyte/dispatch/github

github(options) returns a provider for createDispatch. Authentication is delegated to @sedibyte/auth using a token you already hold. Choose exactly one way to authenticate:

| Option | When to use | | --- | --- | | token: string | The common case. A classic or fine-grained personal access token, or an app installation token. Registers a GitHub API provider with @sedibyte/auth and uses it. | | authProviderName: string | Reuse a GitHub provider you already registered with @sedibyte/auth. | | client: GitHubClient | Inject an existing @sedibyte/auth GitHub client (advanced / testing). |

Extra options: namespace (key on the dispatch object, default "github"), apiBaseUrl (for GitHub Enterprise Server, e.g. https://HOST/api/v3), and userAgent. Credentials are never read from the environment — the caller passes them in.

dispatch.github exposes read and write actions:

  • Read a repogetRepo({ owner, repo })GitHubRepo
  • List reposlistRepos({ org | user | visibility, type?, limit? }) (defaults to the authenticated user; transparently pages unless limit caps it)
  • List commitslistCommits({ owner, repo, sha?, path?, author?, since?, until?, limit? })
  • Read a commitgetCommit({ owner, repo, ref })
  • List pull requestslistPullRequests({ owner, repo, state?, head?, base?, limit? })
  • Read a pull requestgetPullRequest({ owner, repo, number })
  • Open a pull requestcreatePullRequest({ owner, repo, title, head, base, body?, draft? })
  • Push a commitcommitFile({ owner, repo, path, message, content, branch?, sha? }) creates or updates a single file (the Contents API), producing a commit; updating an existing file needs its current blob sha (fetch it with getFileSha({ owner, repo, path, ref? }))
  • Commentcomment({ owner, repo, issueNumber, body }) on an issue or PR

List actions paginate automatically (per_page=100) unless you pass a limit.

Required GitHub token scopes

Grant the token only what the actions you use need. For a classic PAT, repo covers reading and writing private repositories (commits, PRs, comments, contents); public_repo suffices for public repos only. Fine-grained tokens need Contents (read, or read/write for commitFile), Pull requests (read, or read/write to open PRs), and Issues (read/write for comment).

Jira — @sedibyte/dispatch/jira

jira(options) returns a provider for createDispatch. Authentication is delegated to @sedibyte/auth, which supports both Jira Cloud (email + API token, sent as HTTP Basic) and Data Center / Server (a personal access token, sent as Bearer). Choose exactly one way to authenticate:

| Option | When to use | | --- | --- | | { baseUrl, email, apiToken } | Jira Cloud. Registers a Basic-auth provider with @sedibyte/auth and uses it. | | { baseUrl, token } | Jira Data Center / Server. Registers a Bearer (personal-access-token) provider. | | authProviderName: string | Reuse a Jira provider you already registered with @sedibyte/auth. | | client: JiraClient | Inject an existing @sedibyte/auth Jira client (advanced / testing). |

Extra options: namespace (key on the dispatch object, default "jira"), and apiVersion (REST API version path segment, default "3" for Cloud; Data Center commonly uses "2"). Credentials are never read from the environment.

dispatch.jira exposes read actions:

  • Read an issuegetIssue({ key, fields?, expand? })JiraIssue (key is an issue key like "PROJ-123" or a numeric id)
  • Searchsearch({ jql, maxResults?, fields?, nextPageToken? }) → one page of { issues, nextPageToken?, isLast? }
  • Search allsearchAll({ jql, fields?, pageSize? })JiraIssue[], following the cursor through every page for you

Create an API token at id.atlassian.com for Cloud, or a personal access token in your Jira profile for Data Center. The token only needs read access to the projects you query.

Architecture

src/
  core/
    dispatch.ts   createDispatch — composes providers into a typed object
    provider.ts   DispatchProvider contract + defineProvider helper
    errors.ts     DispatchError
  providers/
    teams/
      index.ts    teams() factory (auth wiring via @sedibyte/auth)
      client.ts   createTeamsClient — the actions ({ send })
      types.ts    public option/result types
    discord/
      index.ts    discord() factory (auth wiring via @sedibyte/auth)
      client.ts   createDiscordClient — the actions ({ send, listGuilds, listChannels })
      types.ts    public option/result types
    exchange/
      index.ts    exchange() factory (auth wiring via @sedibyte/auth)
      client.ts   createExchangeClient — the actions ({ email })
      types.ts    public option/result types
    github/
      index.ts    github() factory (auth wiring via @sedibyte/auth)
      client.ts   createGitHubClient — repos/commits/PRs (read + write)
      types.ts    public option/result types
    jira/
      index.ts    jira() factory (auth wiring via @sedibyte/auth)
      client.ts   createJiraClient — the actions ({ getIssue, search, searchAll })
      types.ts    public option/result types

The core knows nothing about any specific SaaS. A provider is just a { namespace, client } pair; createDispatch maps each namespace to its client so dispatch.teams is typed as the Teams client.

Adding a provider

Providers are meant to be copy-pasteable. To add, say, Slack:

  1. Create src/providers/slack/:
    • client.tscreateSlackClient(deps): { send(...) }, the actions. Keep it dependency-injected (pass in the HTTP/auth client) so it's unit-testable without network.
    • types.ts — public option/result types.
    • index.ts — a slack(options) factory that wires up auth/config and returns defineProvider("slack", createSlackClient(...)).
  2. Add a "./slack" entry to exports in package.json.
  3. Ship it with tests (see src/providers/teams/*.test.ts for the pattern).

Nothing in src/core changes.

// src/providers/slack/index.ts (sketch)
import { defineProvider, type DispatchProvider } from "@sedibyte/dispatch";
import { createSlackClient, type SlackClient } from "./client.js";

export function slack(options: SlackOptions): DispatchProvider<"slack", SlackClient> {
  return defineProvider("slack", createSlackClient(/* ...wire up auth/config... */));
}

Development

npm install
npm run typecheck   # tsc --noEmit
npm test            # vitest run
npm run build       # tsc → dist/

License

UNLICENSED — © Sedibyte.