Inbound HTTP → check a tenant-owned vault secret → fire a live onTrigger as that deployment's run principal. The signing cred is only how you got in. The workflow runs as deriveRunPrincipalId(tenantId, runId) — Interchange's mail-triggered grant path, not the credential owner. Credentials stay Interchange's (POST /credentials, credential:*); this package adds POST /api/hooks.
package.json does not declare engines. The published export is TypeScript source (./src/index.ts); Bun consumes it directly. Native Node does not load this extensionless TypeScript source as-is.
npm add @corbits/webhooks
pnpm add @corbits/webhooks
yarn add @corbits/webhooks
bun add @corbits/webhooksinstallWebhooks(opts) mounts POST /api/hooks directly on the host app. Every field of opts is a host responsibility — the same four a hub already has on hand from its own boot sequence (a drizzle handle, its credential cipher, its principal key store, and its mail router):
opts |
Type | What the host provides |
|---|---|---|
app |
{ route(path, handler): unknown } |
Any Hono app; installWebhooks mounts /api/hooks on it. |
db |
DB["db"] (from @intx/db) |
The host's existing drizzle handle. |
credentialCipher |
CredentialCipher (from @intx/types) |
Decrypts the vault secret a hook's credential carries. |
principalKeyStore |
PrincipalKeyStore (from @intx/db) |
Signs the trigger mail as the run's own principal. |
router |
HookMailRouter |
Delivers the trigger mail once a hook fires. A hub backs this with its live sidecar router. |
The function below compiles against this package's real entry point, with every host-owned dependency passed in by its own published type — no in-process store standing in for a hub's database, cipher, key store, or router:
import type { Hono } from "hono";
import type { DB, PrincipalKeyStore } from "@intx/db";
import type { CredentialCipher } from "@intx/types";
import { installWebhooks, type HookMailRouter } from "@corbits/webhooks";
export async function mountWebhooks(
app: Hono,
db: DB["db"],
credentialCipher: CredentialCipher,
principalKeyStore: PrincipalKeyStore,
router: HookMailRouter,
): Promise<void> {
await installWebhooks({ app, db, credentialCipher, principalKeyStore, router });
}Bot tokens for media and chat integrations are separate credentialBindings — not this signing secret.
installWebhooks builds its own deliverer internally from these two exports; a host reaches for them directly only when it is driving trigger mail outside a hook — for example @corbits/cron's ticker points a due schedule at the very same createRunTriggerDeliverer, given a HookMailRouter and a PrincipalKeyStore, so cron and webhooks fire through one system-trigger path. createTenantSystemSender({ db, principalKeyStore }) gives that deliverer a durable per-tenant identity (<senderLocalPart>@domain) the trigger mail is signed and authenticated as. Match a delivery that couldn't route with isRunTriggerUnroutable(error), which narrows to { code, address, runId } — code is RUN_GRANTS_NOT_ROUTABLE or RUN_MAIL_NOT_ROUTABLE.
installWebhooks mounts POST /api/hooks and builds a durable per-tenant system sender (webhook@domain) so the trigger mail's From verifies. Match unroutable deliveries with isRunTriggerUnroutable so callers that speak the MailDeliverer shape (e.g. @corbits/cron) see the same address and runId.
Setting a hook is creating a tenant org credential (principalId null) on a workflow deployed with onTrigger({ on: { type: "mail", to } }). After deploy it has a live address (run_…@domain):
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": "slack",
"providerId": "prv_…",
"type": "api_key",
"secret": "[redacted: looks like a credential]",
"metadata": {
"webhook": {
"verify": "slack",
"workflow": "jimmy"
}
}
}'metadata.webhook.verify is bearer | standard-webhooks | slack (required; there is no none). workflow is a live deployment whose definition or asset name matches; to is a live run address in this tenant. Prefer the credential id (POST $HUB/api/hooks/crd_…); the name form is tenant-scoped (POST $HUB/api/hooks/$TNT/slack). verify: "slack" echoes Slack url_verification (no mail).
git clone https://github.com/corbitsdev/webhooks.git
cd webhooks
bun install
bun run typecheck
bun run testbun run test is bun test ./src.
LGPL-2.1-only.