# Posthook - Dynamic Webhook Receiver A simple API service for receiving and storing webhook requests with dynamic route registration. ## Features - Dynamic route/topic registration - Multiple content type support (JSON, form, text, raw) - CSRF token protection with HMAC-signed stateless tokens - hCaptcha protection per route - ntfy notification alerts when webhooks are received - Form redirect support (`_redirect` field) - Request storage with timestamp-prefixed UUIDs - Per-topic storage organization - TypeScript with Pengueno framework - Comprehensive metrics and tracing - Single Dockerfile deployment ## API Endpoints ### Route Prefixes - **`/`** - Public routes (webhooks, token generation, health) - **`/admin`** - Admin routes (route management) - Put behind OAuth proxy ### Admin Endpoints **⚠️ Recommendation**: Put `/admin/*` behind an OAuth proxy (e.g., OAuth2 Proxy, Pomerium) for authentication. #### Register a Route ```bash POST /admin/routes Content-Type: application/json { "name": "my-webhook", "contentType": "json", "hcaptchaProtected": false } ``` With hCaptcha protection: ```bash POST /admin/routes Content-Type: application/json { "name": "protected-webhook", "contentType": "json", "hcaptchaProtected": true, "hcaptchaSecret": "your-hcaptcha-secret" } ``` With ntfy notifications: ```bash POST /admin/routes Content-Type: application/json { "name": "notified-webhook", "contentType": "json", "hcaptchaProtected": false, "ntfy": { "enabled": true, "server": "https://ntfy.sh", "topic": "my-webhook-alerts" } } ``` With CSRF token protection: ```bash POST /admin/routes Content-Type: application/json { "name": "secure-form", "contentType": "form", "hcaptchaProtected": false, "requireToken": true } ``` #### List Routes ```bash GET /admin/routes ``` ### Public Webhook Endpoints #### Get CSRF Token (for routes with requireToken: true) ```bash GET /hook/{routeName}/token ``` Response: ```json { "ok": { "token": "eyJ0aW1lc3RhbXAiOjE3MDI4MzQ1Njc4OTB5...", "expiresAt": 1702835467890 } } ``` **Note**: Tokens expire in **30 seconds**. Generate the token server-side when rendering your form, not via client-side fetch. #### Send Webhook Send webhooks to registered routes: ```bash POST /hook/{routeName} Content-Type: application/json { "your": "data" } ``` For hCaptcha-protected routes, include the token: ```bash POST /hook/{routeName} H-Captcha-Response: Content-Type: application/json { "your": "data" } ``` For form submissions with redirect (returns 303 redirect instead of JSON): ```bash POST /hook/{routeName} Content-Type: application/x-www-form-urlencoded name=John&email=john@example.com&_redirect=https://example.com/thank-you ``` Or in HTML: ```html
``` For token-protected routes, include the token: ```bash POST /hook/{routeName} X-CSRF-Token: Content-Type: application/json { "your": "data" } ``` Or in form data: ```bash POST /hook/{routeName} Content-Type: application/x-www-form-urlencoded name=John&email=john@example.com&_token= ``` #### Health Check ```bash GET /health ``` ## Content Types - `json` - Parse request body as JSON - `form` - Parse as URL-encoded form data (supports `_redirect` field) - `text` - Store as plain text - `raw` - Store raw body - `multipart` - Multipart form data (not yet implemented, will support `_redirect` field) ## CSRF Token Protection Posthook supports stateless CSRF token protection using HMAC-signed tokens. When `requireToken: true` is set on a route, clients must include a valid token with their request. ### How it works: 1. **Token Generation**: Tokens are HMAC-signed with a secret and contain: - Route name (prevents token reuse across routes) - Timestamp (30-second TTL) 2. **Token Delivery**: Tokens can be provided via: - `_token` field in form data (extracted and not stored) - `X-CSRF-Token` request header 3. **Validation**: Server validates: - HMAC signature - Route name matches - Token not expired (30 seconds) - Timestamp not from future ### Token Secret Configuration: ```bash # Via environment variable (recommended for production) export POSTHOOK_TOKEN_SECRET=your-long-random-secret-here npm start # Via command line npm start -- --token-secret your-long-random-secret-here # Auto-generated (not recommended - changes on restart) npm start # Warns and generates random secret ``` ### Server-Side Token Generation (Recommended): With a 30-second TTL, you should generate the token server-side when rendering the form, not via client-side JavaScript. **Example with server-side rendering**: ```html
``` **Your server** (Node.js example): ```javascript app.get('/contact', async (req, res) => { const response = await fetch('http://localhost:9000/hook/contact-form/token'); const { ok: { token } } = await response.json(); res.render('contact', { token }); }); ``` ## Form Redirects When using `contentType: "form"` or `contentType: "multipart"`, you can include a `_redirect` field in your form data. If the request is successfully stored, the server will respond with a `303 See Other` redirect to the specified URL instead of a JSON response. The `_redirect` field is: - Extracted from the form data and not stored in the request body - Only honored on successful requests (validation failures, errors, etc. will still return JSON error responses) - Useful for traditional HTML form submissions where you want to redirect users after submission Example form flow: 1. User submits form with `_redirect` field (and `_token` if required) 2. Posthook validates token (if required) and stores the request 3. Posthook sends ntfy notification (if configured) 4. Posthook responds with `303 See Other` and `Location: {redirect_url}` header 5. Browser automatically redirects user to the specified URL ## ntfy Notifications When you register a route with ntfy enabled, posthook will send a notification to your specified ntfy server and topic whenever a webhook is received. The notification includes: - Request timestamp - Request UUID - HTTP method - Route name Example notification: ``` Title: Webhook received: my-webhook Message: Method: POST Timestamp: 2024-12-14T19:30:45.123Z UUID: 123e4567-e89b-12d3-a456-426614174000 ``` ## Storage Format Requests are stored per-topic in `data/{routeName}/{timestamp}_{uuid}.json`: ```json { "timestamp": 1702834567890, "uuid": "123e4567-e89b-12d3-a456-426614174000", "routeName": "my-webhook", "method": "POST", "headers": { "content-type": "application/json" }, "body": { "your": "data" } } ``` ## Usage ### Development ```bash npm install npm run build export POSTHOOK_TOKEN_SECRET=your-secret-here npm start -- --port 9000 --host 0.0.0.0 --data-dir ./data ``` ### Docker ```bash docker build -t posthook . docker run -p 9000:9000 \ -v $(pwd)/data:/app/data \ -e POSTHOOK_TOKEN_SECRET=your-secret-here \ posthook ``` ### With OAuth Proxy (Recommended for Production) Protect admin routes with an OAuth proxy: ```bash # Run posthook docker run -p 9000:9000 posthook # Run OAuth2 Proxy in front of /admin routes docker run -p 4180:4180 \ quay.io/oauth2-proxy/oauth2-proxy:latest \ --upstream=http://localhost:9000/admin \ --http-address=0.0.0.0:4180 \ --provider=google \ --client-id=your-client-id \ --client-secret=your-client-secret \ --cookie-secret=random-secret-here \ --email-domain=yourdomain.com ``` Then configure your reverse proxy: - `/admin/*` → OAuth2 Proxy on port 4180 - `/` → Posthook on port 9000 ## Configuration ### Command Line Arguments: - `--port` - Server port (default: 9000) - `--host` - Server host (default: 0.0.0.0) - `--data-dir` - Data storage directory (default: ./data) - `--token-secret` - HMAC secret for token signing (optional, generates random if not provided) ### Environment Variables: - `POSTHOOK_TOKEN_SECRET` - HMAC secret for token signing (recommended for production)