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

@corbits/webhooks

v0.3.0

Published

Receive signed webhooks (Slack, Standard Webhooks, bearer) and start a workflow run.

Readme

@corbits/webhooks

Signed webhook ingress for Interchange workflows: verifies a Slack, Standard Webhooks or bearer request against a hub credential, then delivers the body as trigger mail to a live workflow run. A Corbits hub module, mounted as Hono routes on the Interchange hub (Interchange's multi-tenant control plane, where a principal is an identity and a grant is a permission it holds) and backed by its Postgres.

Why @corbits/webhooks?

  1. Secrets stay in the hub. Each hook is an ordinary Interchange credential with metadata.webhook. The only thing this package stores is a short-lived table of seen delivery ids.
  2. Runs never borrow a person's authority. The workflow runs as its own run principal (the identity Interchange derives for each run) through the hub's mail-triggered grant path, never as the credential owner.
  3. Three verifiers, no unsigned mode. Slack signatures, Standard Webhooks HMAC and bearer tokens. A credential without a verifier matches no hook.

Use it to start workflows from third-party events. It does not fire on a schedule; @corbits/cron does, through a deliverer of the same shape.

Install

bun add @corbits/webhooks \
  @intx/crypto @intx/db @intx/hub-api @intx/hub-common @intx/mime @intx/types drizzle-orm hono postgres

The @intx/* peers are ^0.4.0, drizzle-orm is ^0.45.1, hono is ^4.11.9 and postgres is ^3.4.8.

Where it fits

  • Hub side. Routes mount on the hub's Hono app. Credentials and tenants come from @intx/db; run grants come from @intx/hub-api's mail-triggered materializer.
  • Sidecar side. Trigger mail and run grants reach the run's sidecar (the agent runtime) through the hub's router.
  • Siblings. @corbits/cron fires scheduled runs through the same MailDeliverer contract; createRunTriggerDeliverer builds one.

Reference

Routes

| Route | Resolves the hook by | | --------------------------------- | ----------------------------------------------------------------------------- | | POST /api/hooks/:id | Credential id (crd_…). Preferred. | | POST /api/hooks/:tenantId/:name | Credential name, scoped to the tenant. | | POST /api/hooks | x-webhook-hook header or ?hook=; tenant from x-tenant-id or ?tenant=. |

Responses: 202 delivered, 200 { challenge } for Slack url_verification, 401 bad signature, 404 unknown hook (misses and name collisions look the same), 409 more than one live run matches, 503 no live run or delivery failed, 500 credential lookup failed.

metadata.webhook

| Field | Value | | ---------- | -------------------------------------------------------------- | | verify | "bearer", "standard-webhooks" or "slack". Required. | | workflow | Name of a live deployment's definition or asset in the tenant. | | to | A live run address in the tenant, instead of workflow. |

With neither set, the credential name is matched against definition and asset names, then the tenant's only live run. A live run has status deployed or running.

With standard-webhooks, the secret is base64-decoded after stripping an optional whsec_ prefix, as the spec requires. An unprefixed secret that is valid base64 is tried decoded first, then as raw bytes, which is how 0.1 read it; one that is not base64 is only tried raw. An empty secret never verifies, under any scheme.

A replayed delivery gets 409. Standard Webhooks deliveries are keyed on the credential and webhook-id, Slack on the credential and signature, each kept until its timestamp leaves the ±300s window. The seen-set is the replay table in the webhooks schema, created by runWebhookMigrations (see Using with Interchange), so every replica sharing the database rejects the replay. A failed delivery releases its key so the sender's retry goes through. If the hub crashes between claiming a key and forwarding the delivery, the key stays held until it expires with its timestamp window, so the sender's retries get 409 until then. Bearer requests carry no id and are not deduplicated.

Signatures are checked over the raw request bytes, and the body is forwarded unchanged. Bodies over 1 MiB get 413; a verified body that is not UTF-8 gets 415. Bot tokens for chat integrations belong in the workflow's credentialBindings, not in the signing secret.

Exports

| Export | Purpose | | ----------------------------------------------------- | -------------------------------------------------------------------------- | | createHookRoutes(deps) | The hook routes as a Hono sub-app. | | createRunTriggerDeliverer(opts) | Delivers trigger mail to a live run as its run principal. | | createTenantSystemSender({ db, principalKeyStore }) | Durable per-tenant sender (<localPart>@domain) that signs trigger mail. | | isRunTriggerUnroutable(error) | Narrows to { code, address, runId } when the router has no route. | | RUN_MAIL_NOT_ROUTABLE | The error code. | | RunTriggerUnroutableError | The error isRunTriggerUnroutable matches. | | HookRouter | Pick<SidecarRouter, "routeMail">, the router the host passes in. | | MailDeliverer | { to(address, content, tenantId, subject) }, what the deliverer returns. |

runWebhookMigrations(dbConfig, { schema })

From @corbits/webhooks/migrations. Run it after Interchange's runMigrations, with the same config and the same schema: the host schema that holds the credential table. The replay table always lives in the webhooks schema. It is idempotent and takes an advisory lock, so several replicas can start at once.

Using with Interchange

Run runWebhookMigrations at hub start, after runMigrations, then build the routes from the hub's database, credential cipher and principal key store:

import { createEnvKeyCredentialCipher } from "@intx/crypto";
import { createDB, createPrincipalKeyStore, runMigrations } from "@intx/db";
import { hexDecode } from "@intx/types";
import { createHookRoutes, type HookRouter } from "@corbits/webhooks";
import { runWebhookMigrations } from "@corbits/webhooks/migrations";

const dbConfig = {
  host: "localhost",
  port: 5432,
  user: "postgres",
  password: "postgres",
  database: "interchange",
};
await runMigrations(dbConfig, { schema: "public" });
await runWebhookMigrations(dbConfig, { schema: "public" });

const { db } = createDB(dbConfig);

export const hookRoutes = (router: HookRouter) =>
  createHookRoutes({
    db,
    credentialCipher: createEnvKeyCredentialCipher(
      hexDecode(String(process.env["CREDENTIAL_ENCRYPTION_KEY"])),
    ),
    principalKeyStore: createPrincipalKeyStore({
      db,
      cipher: createEnvKeyCredentialCipher(
        hexDecode(String(process.env["PRINCIPAL_KEY_ENCRYPTION_KEY"])),
      ),
    }),
    router,
  });

router is the hub's sidecar mail router. Mount the result at /api/hooks outside the hub's session and tenant middleware: senders carry a signature, not a session, and the tenant comes from the hook's credential.

Deploy a workflow that uses onTrigger({ on: { type: "mail", to } }) from @intx/workflow. Then create a provider and a tenant credential that points at it:

curl -X POST "$HUB/api/tenants/$TNT/providers" \
  -H "content-type: application/json" -H "cookie: $COOKIE" \
  -d '{"name":"webhooks","plugin":"api_key"}'

curl -X POST "$HUB/api/tenants/$TNT/credentials" \
  -H "content-type: application/json" -H "cookie: $COOKIE" \
  -d '{
    "name": "my-hook",
    "providerId": "prv_…",
    "type": "api_key",
    "secret": "'"$SECRET"'",
    "metadata": { "webhook": { "verify": "standard-webhooks", "workflow": "my-workflow" } }
  }'

Send a signed POST to the credential id the second call returned:

ID=msg_1 TS=$(date +%s) BODY='{"text":"hi"}'
SIG=$(printf '%s' "$ID.$TS.$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)

curl -X POST "$HUB/api/hooks/crd_…" \
  -H "content-type: application/json" \
  -H "webhook-id: $ID" -H "webhook-timestamp: $TS" -H "webhook-signature: v1,$SIG" \
  -d "$BODY"

The hub answers 202 { "ok": true, "to": "run_…@acme.example" } and the run starts.

Upgrading from 0.2

  • HookMailRouter is replaced by HookRouter, Pick<SidecarRouter, "routeMail"> from @intx/hub-sessions. Pass the hub's sidecar router as before; sendRunGrants is no longer used.
  • Run grants and trigger mail go out in one routeMail call, so RUN_GRANTS_NOT_ROUTABLE and RunTriggerUnroutableCode are removed. Every unroutable delivery throws RunTriggerUnroutableError with code RUN_MAIL_NOT_ROUTABLE, and new RunTriggerUnroutableError(address, runId) no longer takes a code.
  • isRunTriggerUnroutable uses instanceof; every consumer must resolve one copy of @corbits/webhooks.
  • @intx/hub-sessions is a new peer dependency.

Upgrading from 0.1

  • installWebhooks(opts) is removed. Call app.route("/api/hooks", createHookRoutes(deps)) with the same db, credentialCipher, principalKeyStore and router; drop app from the options.
  • @intx/* and hono moved to peer dependencies. Add them to the host.
  • Run runWebhookMigrations at hub start, after runMigrations; replay protection needs its table.
  • Existing hook credentials and URLs keep working unchanged when the routes stay mounted at /api/hooks.

License

LGPL-2.1-only.