- TypeScript 73.4%
- Go 26.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .github/workflows | ||
| worker | ||
| .gitignore | ||
| .golangci.yml | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
| relay.go | ||
| relay_test.go | ||
relay
Webhooks for the desktop apps (keybars Pro, later shepherd Pro). A desktop app has no address GitHub or Forgejo can reach, so the relay gives it one: it checks each delivery's signature, cuts the payload down to the fields a rule matches on, and passes that to the app over the WebSocket the app holds open.
worker/: the Cloudflare Worker atrelay.jwarykowski.dev, with a Durable Object per hook.- The Go package: apps make and remove hooks, and hold the connection.
var Hooks = relay.Product{ID: "keybars-pro"}
h, err := Hooks.Create(ctx, "github") // h.URL and h.Secret go into GitHub's webhook settings
go Hooks.Run(ctx, h, func(ev relay.Event) { /* ev.Name: pull_request.merged */ }, onState)
Keep hooks in Product.Path() (hooks.json in the app's config directory,
mode 600) with Save, never in a settings file a user might share.
RELAY_SERVER points the client at another relay.
What it keeps
Each hook stores its product, provider, secret and a hash of the app's token, nothing else. Deliveries aren't stored: one that arrives while the app is offline is dropped. A hook nobody connects to for 90 days deletes itself.
From a delivery the app gets the event, the repository, the branch and the pull request or issue number:
{ "source": "github", "name": "pull_request.merged", "repo": "jwarykowski/keybars", "branch": "main", "number": 42 }
| GitHub event | Becomes |
|---|---|
pull_request |
pull_request.opened, .merged, .closed, .review_requested (branch: the base) |
pull_request_review |
review.approved, review.changes_requested |
workflow_run |
ci.running, ci.passed, ci.failed (branch: the run's) |
push |
push (branches only) |
release |
release.published |
issues |
issue.opened |
ping |
ping, so the app can tell the webhook works |
Forgejo's events (the "Forgejo" webhook type, signed with
X-Forgejo-Signature) take the same names, so a rule can say "CI failed"
for either: pull requests, pushes, releases and issues as above, reviews
from pull_request_review_approved and _rejected, and action runs
(Forgejo 12 or later) from action_run_failure to ci.failed and
action_run_success or _recover to ci.passed, on the branch that
started the run. Forgejo sends no event when a run starts, so there's no
ci.running; its test delivery is a push.
Any webhook (provider webhook) is for services the relay doesn't know: the
event's name is the last part of the address, /h/<id>/deploy.done, or a
JSON body's "event", with "repo", "branch" and "number" if wanted.
Names are letters, digits, dots, dashes and underscores. A GET works too,
with the name in the address, for services that can only fetch a URL. The
address is the secret unless the app turns on requireSecret; then the
relay refuses anything without Authorization: Bearer <secret>.
API
POST /v1/<product>/hooks {"provider":"github"}, forgejo or webhook |
a new hook: { id, url, token, secret }; 10 a minute per IP |
GET /v1/<product>/hooks/<id>/connect |
the app's WebSocket, with Authorization: Bearer <token>; send ping to keep it open |
DELETE /v1/<product>/hooks/<id> |
forget the hook, with the token |
PATCH /v1/<product>/hooks/<id> {"requireSecret":true} |
any webhook: refuse deliveries without the secret, with the token |
POST /h/<id>, /h/<id>/<event> |
the provider's deliveries, signed with the secret; any webhook takes GET with an event too |
Adding a product or provider
- A product: add its ID to
PRODUCTSinworker/wrangler.jsonc, thennpm run deploy. - A provider: a
Providerinworker/src/providers/(how to check a delivery's signature, and which events it becomes), listed inproviders/index.ts.
Develop
go test ./...
cd worker
npm install
npm test # Vitest, with a fake Durable Object state
npm run check && npm run lint && npm run fmt:check
npm run dev # wrangler dev on :8787
RELAY_LIVE=1 RELAY_SERVER=http://127.0.0.1:8787 go test -run TestAgainstRelay ./... # from the root, with dev running
npm run deploy