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
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ body:
attributes:
label: webhookd version
description: Output of `webhookd --version`
placeholder: "v0.1.0"
placeholder: "0.1.0"
validations:
required: true

Expand Down Expand Up @@ -76,4 +76,4 @@ body:
description: Flags passed to webhookd. Redact secret values.
placeholder: "webhookd github --port 8080 --path /hooks/github"
validations:
required: false
required: false
8 changes: 5 additions & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ updates:
commit-message:
prefix: "chore"
ignore:
# webhookd uses standard library only in core.
# Any new dependency is a signal to review carefully.
# Major-version bumps are never automatic. The dependency surface
# is small — cobra in cmd/, plus its transitive deps pflag and
# mousetrap — and any new require entry is a decision that should
# be reviewed, not merged by a bot.
- dependency-name: "*"
update-types: ["version-update:semver-major"]

Expand All @@ -25,4 +27,4 @@ updates:
schedule:
interval: monthly
commit-message:
prefix: "chore"
prefix: "chore"
4 changes: 2 additions & 2 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Skip this section if this is not a provider PR.
- [ ] Official webhook documentation link:
- [ ] `docs/providers/<name>.md` written from `docs/providers/TEMPLATE.md`, every section filled
- [ ] All items on the self-review checklist in `CONTRIBUTING.md` are checked
- [ ] No new dependencies in `go.mod`
- [ ] Provider code imports only standard-library packages
- [ ] Test vectors are real — taken from the provider's official docs, not fabricated
- [ ] Both valid-signature and tampered-body test cases are present

Expand All @@ -48,4 +48,4 @@ Skip this section if this is not a provider PR.
- [ ] `make check` passes locally with no errors
- [ ] PR title follows Conventional Commits (`feat:`, `fix:`, `docs:`, `test:`, `refactor:`, `chore:`, `ci:`)
- [ ] Nothing is written to stdout except through the JSONL event writer
- [ ] No secrets, tokens, or real webhook payloads are committed
- [ ] No secrets, tokens, or real webhook payloads are committed
12 changes: 7 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.23'
go-version-file: "go.mod"
cache: true
- name: Run go vet
run: go vet ./...
- uses: golangci/golangci-lint-action@v6
with:
version: latest
Expand All @@ -37,7 +39,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.23'
go-version-file: "go.mod"
cache: true
- name: Run tests with race detector
run: go test -race -count=1 -coverprofile=coverage.out ./...
Expand All @@ -63,13 +65,13 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.23'
go-version-file: "go.mod"
cache: true
- name: Build
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: '0'
CGO_ENABLED: "0"
run: go build -o /dev/null .

commitlint:
Expand All @@ -82,4 +84,4 @@ jobs:
fetch-depth: 0
- uses: wagoid/commitlint-github-action@v6
with:
configFile: .commitlintrc.json
configFile: .commitlintrc.json
7 changes: 4 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches: [main]
schedule:
# Weekly on Monday at 06:00 UTC
- cron: '0 6 * * 1'
- cron: "0 6 * * 1"

permissions:
security-events: write
Expand All @@ -24,7 +24,8 @@ jobs:

- uses: actions/setup-go@v5
with:
go-version: '1.23'
# Single source of truth: go.mod's go directive.
go-version-file: "go.mod"
cache: true

- uses: github/codeql-action/init@v3
Expand All @@ -35,4 +36,4 @@ jobs:

- uses: github/codeql-action/analyze@v3
with:
category: "/language:go"
category: "/language:go"
16 changes: 13 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: release
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- "v[0-9]+.[0-9]+.[0-9]+"

permissions:
contents: write
Expand All @@ -22,9 +22,19 @@ jobs:

- uses: actions/setup-go@v5
with:
go-version: '1.23'
go-version-file: "go.mod"
cache: true

# dockers_v2 builds multi-platform images with buildx. QEMU
# provides the arm64 emulation the amd64 runner lacks; buildx
# provides a builder capable of producing a multi-platform
# manifest in a single pass.
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- uses: docker/login-action@v3
with:
registry: ghcr.io
Expand All @@ -36,4 +46,4 @@ jobs:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43 changes: 43 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Configuration for golangci-lint.
#
# The default set of linters (errcheck, govet, ineffassign, staticcheck,
# unused) is enabled via `default: standard`. Four extras are added:
#
# bodyclose — catches HTTP response bodies that are not closed, which
# matters for the client-side code in server_test.go.
# gosec — security-focused checks. This tool verifies signatures;
# a dependency-free security linter is cheap insurance.
# misspell — catches typos in comments and string literals.
# gofmt / — formatters, not linters. Included so CI catches files
# goimports that were saved without running `make fmt` locally.
#
# The Makefile runs this via `golangci-lint run ./...`, as the `lint`
# target. `make check` chains fmt → vet → lint → test.

version: "2"

run:
# The default 1m timeout is tight for a cold CI run that includes
# race-instrumented tests. 5m gives headroom without being effectively
# unbounded.
timeout: 5m
tests: true

linters:
default: standard
enable:
- bodyclose
- gosec
- misspell

formatters:
enable:
- gofmt
- goimports

issues:
# Do not cap the number of issues reported. The codebase is small; if
# a change produces fifty warnings, the reviewer wants to see all
# fifty, not a truncated list.
max-same-issues: 0
max-issues-per-linter: 0
50 changes: 50 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
version: 2

builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
ldflags:
- -s -w -X main.version={{.Version}}

archives:
- formats: [tar.gz]
format_overrides:
- goos: windows
formats: [zip]

# dockers_v2 replaced dockers + docker_manifests. It runs a single
# `docker buildx build` per image, producing a multi-arch manifest
# without intermediate per-arch tags. GoReleaser stages every artifact
# for the target platform under $TARGETPLATFORM/ in a temporary build
# context, which is what the Dockerfile copies from.
#
# Images are built and pushed in the publish phase, not the build
# phase. `goreleaser build` and `goreleaser release --skip=publish`
# do not produce images.
dockers_v2:
- images:
- ghcr.io/0xprogress/webhookd
tags:
- "{{.Version}}"
- latest
platforms:
- linux/amd64
- linux/arm64

checksum:
name_template: checksums.txt

changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- "^chore:"
111 changes: 111 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Changelog

All notable changes to webhookd are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---

## [Unreleased]

### Planned

- `github` provider — HMAC-SHA256 over the raw body, `X-Hub-Signature-256`
- `stripe` provider — HMAC-SHA256 over `{timestamp}.{body}`, 300s timestamp check
- `slack` provider — HMAC-SHA256 over `v0:{timestamp}:{body}`, 300s timestamp check
- `shopify` provider — base64-encoded HMAC-SHA256 over the raw body

Nothing is committed to any of the above. Each provider is a separate PR
against a frozen core; see [CONTRIBUTING.md](CONTRIBUTING.md).

---

## [0.1.0]

The first release. This is the core: a working webhook receiver that verifies
signatures, streams verified events as JSONL on stdout, and does nothing else.

### Added

**Core pipeline**

- HTTP listener with read and write timeouts of 10 seconds
- Raw request body captured once and never re-encoded — the same bytes are
passed to signature verification and emitted as `payload`
- Body size limit of 2 MB, enforced during the read via `http.MaxBytesReader`
- Requests rejected with the correct status for every failure mode: 401 for a
bad signature, 404 for an unknown provider, 405 for a non-POST method, 413
for an oversized body, 415 for an unsupported content type, 500 for an
internal error
- Health endpoint at `GET /health`
- Default bind address of `127.0.0.1`; binding to `0.0.0.0` requires an
explicit `--host 0.0.0.0`

**Provider interface and registry**

- The `Provider` interface — five methods a provider implements to answer
five questions about a request
- Provider registry with self-registration via `init()`
- Registry panics on duplicate provider names, catching a programming error
at startup rather than at request time
- The interface is frozen at v0.1

**Output**

- JSONL event stream on stdout, one object per line, identical shape for every
provider
- Output preserves key order, duplicate keys, and numeric precision from the
request body — the payload is emitted from the raw bytes, not re-encoded
from a decoded map
- Pretty mode (`--pretty`) for human-readable output during live demos
- All diagnostics — startup banner, errors, signature failures — on stderr;
nothing but data on stdout

**CLI**

- `webhookd <provider>` to run a provider receiver
- `webhookd --list` to print registered providers
- `webhookd --version` to print the version
- Flags: `--secret-env`, `--port`, `--host`, `--path`, `--pretty`,
`--max-body`, `--timeout`
- Configuration resolves in the order flag → environment → default
- Secrets are passed by name (`--secret-env STRIPE_WEBHOOK_SECRET`) and read
from the environment; the value never appears on the command line

**Mock provider**

- A reference implementation of the `Provider` interface
- Accepts any request with the header `X-Mock-Signature: valid`
- Ships in every build; used by the server test suite
- Not intended for production

**Documentation**

- `README.md` with a five-minute demo that works end to end
- `CONTRIBUTING.md` covering the provider contribution process
- `docs/architecture.md` explaining the core/provider split
- `docs/webhookd-core.md` as the build specification
- `docs/providers/checklist.md` as a copy-pasteable self-review list
- `docs/providers/TEMPLATE.md` as the provider documentation template
- `SECURITY.md` with a vulnerability reporting process

**Distribution**

- Cross-compiled binaries for linux/amd64, linux/arm64, darwin/amd64,
darwin/arm64, windows/amd64, windows/arm64
- Multi-architecture Docker image on `ghcr.io/0xprogress/webhookd`

### Security

- Signature comparison uses `hmac.Equal()` in every provider, avoiding timing
side channels
- Timestamps in providers that use them are rejected if older than 300 seconds,
protecting against replay
- Request bodies are capped at 2 MB, enforced before the body is fully read
- Secrets are read from environment variables, never from flags

---

[Unreleased]: https://github.com/0xProgress/webhookd/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/0xProgress/webhookd/releases/tag/v0.1.0
Loading
Loading