Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: ci

on:
pull_request:
push:
branches: [main]

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:16
env: { POSTGRES_PASSWORD: postgres, POSTGRES_DB: webhooks }
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
env:
PGHOST: localhost
PGPORT: 5432
PGUSER: postgres
PGPASSWORD: postgres
PGDATABASE: webhooks
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: 24
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run check
- run: bun run test:e2e
- name: node consumer smoke
run: |
set -euo pipefail
TARBALL="$PWD/$(npm pack --silent)"
mkdir -p "$RUNNER_TEMP/c" && cd "$RUNNER_TEMP/c"
npm init -y >/dev/null && npm pkg set type=module >/dev/null
npm install "$TARBALL"
node -e 'import("@corbits/webhooks").then((m) => { for (const n of ["createHookRoutes", "createRunTriggerDeliverer", "createTenantSystemSender", "isRunTriggerUnroutable"]) if (typeof m[n] !== "function") throw new Error("missing export: " + n); })'
53 changes: 53 additions & 0 deletions .github/workflows/cla.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: CLA Assistant

on:
issue_comment:
types: [created]
pull_request_target:
types: [opened, synchronize]

permissions:
actions: write
contents: write
pull-requests: write
statuses: write

concurrency:
group: cla-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
cancel-in-progress: true

jobs:
cla:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Maintainer fast path
if: >-
github.event_name == 'pull_request_target' &&
(contains(fromJSON('["TheGreatAxios","brianjfox"]'), github.event.pull_request.user.login) ||
endsWith(github.event.pull_request.user.login, '[bot]'))
run: echo "Maintainer or bot pull request; CLA not required."
- name: CLA Assistant
if: >-
((github.event.comment.body == 'recreate-signatures' ||
github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') ||
github.event_name == 'pull_request_target') &&
!(github.event_name == 'pull_request_target' &&
(contains(fromJSON('["TheGreatAxios","brianjfox"]'), github.event.pull_request.user.login) ||
endsWith(github.event.pull_request.user.login, '[bot]')))
# corbitsdev/cla-assistant-action v2.6.1-node24: upstream v2.6.1 on node24.
uses: corbitsdev/cla-assistant-action@ef6d3e51db8232fe93090810f13bde30497c1d74
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
path-to-document: "https://github.com/${{ github.repository }}/blob/main/CLA.md"
path-to-signatures: "signatures/version1/cla.json"
branch: "cla-signatures"
allowlist: TheGreatAxios,brianjfox,*[bot]
custom-notsigned-prcomment: >-
Thank you for your contribution. Before it can be merged, please read our
[Contributor License Agreement](https://github.com/${{ github.repository }}/blob/main/CLA.md)
and sign it by posting a new comment on this pull request containing
exactly the line below (nothing else):
custom-pr-sign-comment: "I have read the CLA Document and I hereby sign the CLA"
custom-allsigned-prcomment: "All contributors have signed the CLA."
9 changes: 5 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
node_modules
dist
.tmp
node_modules/
dist/
*.tsbuildinfo
*.tgz
coverage/
.env
.env.*
.DS_Store
*.log
40 changes: 40 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# AGENTS.md

## Purpose

`@corbits/webhooks` is signed webhook ingress for Interchange workflows. It
owns the hook routes, the Slack, Standard Webhooks and bearer verifiers, hook
resolution, and delivery of the body as trigger mail to a live run. It does not
own credentials (they are Interchange credentials with `metadata.webhook`),
storage of its own, or scheduling (`@corbits/cron`).

## Layout

- `src/routes.ts`: `createHookRoutes`, the Hono sub-app for `/api/hooks`.
- `src/verify.ts`: the three signature verifiers.
- `src/resolve.ts`: credential lookup and target run resolution.
- `src/hooks.ts`: the request handler that ties verify, resolve and deliver.
- `src/deliver.ts`: `createRunTriggerDeliverer` and the unroutable errors.
- `src/system-sender.ts`: `createTenantSystemSender`, the per-tenant signer of
trigger mail.
- `src/index.ts`: the only module consumers import from.
- `e2e/`: real-Postgres suites, gated on the libpq `PG*` variables.

## Rules

- The signing credential only authenticates the request. The workflow runs as
its own run principal, `deriveRunPrincipalId(tenantId, runId)`, through
Interchange's mail-triggered grant path, never as the credential owner.
- Trigger mail is signed by the durable per-tenant system sender
(`webhook@domain`), whose key is co-delivered on the run-grants barrier.
- No unsigned mode: a credential without a verifier matches no hook.
- Unknown hooks and name collisions both answer `404`.
- `exactOptionalPropertyTypes` is on: omit optional keys, never assign
`undefined` to them.

## Local development

```sh
bun install
bun run check
```
64 changes: 64 additions & 0 deletions CLA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Contributor License Agreement (CLA)

Version 1.0

This Contributor License Agreement ("Agreement") is entered into between the contributor ("Contributor") and ABK Labs, Inc. ("Maintainer").

By submitting any Contribution to a project maintained by Maintainer, the Contributor agrees to the following terms.

## 1. Definitions

"Contribution" means any source code, object code, documentation, test cases, designs, specifications, bug fixes, enhancements, comments, pull requests, commits, issues containing code, or other materials intentionally submitted to a project maintained by Maintainer.

## 2. Copyright Ownership

The Contributor retains ownership of the Contributor's copyrights in the Contribution.

No transfer of copyright ownership is required by this Agreement.

## 3. License Grant to Maintainer

The Contributor grants Maintainer a perpetual, worldwide, non-exclusive, irrevocable, royalty-free license to:

- use, reproduce, modify, distribute, display, perform, and sublicense the Contribution;
- combine the Contribution with other software and works;
- distribute the Contribution under the project's current license;
- distribute the Contribution under future versions of the project's license;
- distribute the Contribution under alternative open-source, source-available, commercial, proprietary, or dual-license terms.

## 4. License Grant to the Public

The Contributor agrees that the Contribution may be distributed as part of the applicable project under the project's then-current license terms, including LGPL-2.1 or any successor license adopted by Maintainer.

## 5. Contributor Representations

The Contributor represents and warrants that:

1. The Contributor created the Contribution or otherwise has sufficient rights to submit it.
2. The Contributor has the legal authority to grant the rights described in this Agreement.
3. To the best of the Contributor's knowledge, the Contribution does not knowingly infringe the intellectual property rights of any third party.
4. Any third-party material included in the Contribution has been properly disclosed and is compatible with the rights granted under this Agreement.

## 6. Corporate Contributions

If a Contribution is submitted on behalf of an employer or other legal entity, the person accepting this Agreement represents that they are authorized to bind that entity to this Agreement.

## 7. No Obligation

Maintainer is not obligated to use, distribute, maintain, support, or accept any Contribution.

## 8. Disclaimer

Except as expressly stated in this Agreement, the Contribution is provided "AS IS" without warranties or conditions of any kind.

## 9. Electronic Acceptance

The Contributor agrees that electronic acceptance of this Agreement, including acceptance through a website, source-control platform, click-through process, pull-request workflow, or similar mechanism, shall have the same force and effect as a handwritten signature.

## 10. Governing Law

This Agreement shall be governed by the laws of the State of California, excluding its conflict-of-law provisions.

---

By submitting a Contribution, the Contributor acknowledges that they have read and agree to this Agreement.
35 changes: 25 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
# Contributing

## Running the tests
## Development

```sh
git clone https://github.com/corbitsdev/webhooks.git
cd webhooks
bun install
bun run typecheck
bun run test
bun run test:e2e
bun run check
```

`bun run test` runs the `./src` units and `bun run test:e2e` the `./e2e` real-Postgres suites, which run when `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD` and `PGDATABASE` are all set and skip otherwise. Each suite migrates a throwaway `wh_*` schema and drops it on exit; a killed run can leave one behind.
`bun run check` runs typecheck, lint, format check and unit tests. `bun run format` rewrites the tree.

## How a hook fires
Contributors sign the [CLA](CLA.md) on their first PR; the CLA bot explains how.

The signing credential only authenticates the inbound request. The workflow runs as its own run principal, `deriveRunPrincipalId(tenantId, runId)`, through Interchange's mail-triggered grant path (`createMailTriggeredRunGrantsMaterializer`), never as the credential owner. Credentials stay Interchange's (`POST /credentials`, `credential:*`); this package only adds the hook routes.
`bun run test:e2e` runs the real-Postgres suites in `e2e/`. They run when
`PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD` and `PGDATABASE` are all set and skip
otherwise. Each suite migrates a throwaway `wh_*` schema and drops it on exit; a
killed run can leave one behind.

The trigger mail is signed by a durable per-tenant system sender (`webhook@domain`), a `kind: "user"` principal whose key is co-delivered on the run-grants barrier so the recipient verifies the mail's `From`.
```sh
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres:16
PGHOST=localhost PGPORT=5432 PGUSER=postgres PGPASSWORD=postgres PGDATABASE=postgres bun run test:e2e
```

## Commit messages

Commit subjects and PR titles follow [Conventional Commits](https://www.conventionalcommits.org): `feat`, `fix`, `refactor`, `test`, `docs`, `build`, `ci`, `perf`, and `chore(release): x.y.z` for releases.
Add `!` only for public API breaks: removed or renamed exports, changed signatures, newly required params. Peer and dependency range changes are `build(deps):` with no `!`.
Keep subjects imperative, lowercase after the colon, 72 characters or less, and free of ticket IDs.
Every PR links its issue with a `Closes <issue id>` line in the PR body.

## Releasing

Releases are manual. On a clean, up-to-date `main`:

```sh
npm version <patch|minor> -m "chore(release): %s"
git push --follow-tags
gh release create "v$(node -p 'require("./package.json").version')" --generate-notes
npm publish
```

Bump minor only for breaking API changes; everything else is a patch. `prepack` builds `dist/` from the tagged commit.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# @corbits/webhooks

[![npm](https://img.shields.io/npm/v/@corbits/webhooks.svg)](https://www.npmjs.com/package/@corbits/webhooks) [![License: LGPL-2.1](https://img.shields.io/badge/license-LGPL--2.1-green.svg)](https://github.com/corbitsdev/webhooks/blob/main/LICENSE)

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?
Expand Down
27 changes: 19 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
"name": "@corbits/webhooks",
"version": "0.2.0",
"description": "Receive signed webhooks (Slack, Standard Webhooks, bearer) and start a workflow run.",
"homepage": "https://github.com/corbitsdev/webhooks",
"keywords": [
"corbits",
"hono",
"interchange",
"slack",
"standard-webhooks",
"webhooks"
],
"homepage": "https://github.com/corbitsdev/webhooks#readme",
"bugs": {
"url": "https://github.com/corbitsdev/webhooks/issues"
},
Expand All @@ -19,8 +27,6 @@
],
"type": "module",
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand All @@ -31,14 +37,15 @@
"access": "public"
},
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json",
"build": "rm -rf dist && tsc -p tsconfig.build.json",
"prepack": "bun run build",
"test": "bun test ./src",
"test:e2e": "bun test ./e2e",
"typecheck": "tsc --noEmit",
"lint": "oxlint",
"format": "oxfmt",
"format:check": "oxfmt --check"
"format:check": "oxfmt --check",
"test": "bun test src --pass-with-no-tests",
"test:e2e": "bun test e2e",
"check": "bun run typecheck && bun run lint && bun run format:check && bun run test"
},
"devDependencies": {
"@intx/crypto": "0.4.0",
Expand All @@ -62,5 +69,9 @@
"@intx/mime": "^0.4.0",
"@intx/types": "^0.4.0",
"hono": "^4.11.9"
},
"engines": {
"bun": ">=1.2.0",
"node": ">=24"
}
}
10 changes: 4 additions & 6 deletions tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@
"declaration": true,
"declarationMap": false,
"sourceMap": false,
"composite": false,
"outDir": "dist",
"rootDir": "src",
"allowImportingTsExtensions": false,
"module": "NodeNext",
"moduleResolution": "NodeNext"
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts"],
"include": ["src"],
"exclude": ["src/**/*.test.ts"]
}
18 changes: 10 additions & 8 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ESNext"],
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"resolveJsonModule": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"skipLibCheck": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"noEmit": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
"lib": ["ESNext"],
"types": ["bun"]
},
"include": ["src/**/*.ts", "e2e/**/*.ts"]
"include": ["src", "e2e"]
}
Loading