# Posthook - Dynamic Webhook Receiver A simple API service for receiving and storing webhook requests with dynamic route registration. ## Features - File-based route configuration with hot-reload - Multiple content type support (JSON, form, text, raw, multipart) - 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 ## Configuration Routes are configured via a `routes.toml` file that Posthook watches for changes. When you edit the file, Posthook automatically reloads the configuration. ### Example Configuration Create a `routes.toml` file: ```toml # Simple JSON webhook [[route]] name = "github-webhook" contentType = "json" hcaptchaProtected = false requireToken = false # Form with hCaptcha protection [[route]] name = "contact-form" contentType = "form" hcaptchaProtected = true hcaptchaSecret = "0x0000000000000000000000000000000000000000" requireToken = false # JSON webhook with ntfy notifications [[route]] name = "alerts" contentType = "json" hcaptchaProtected = false requireToken = false [route.ntfy] enabled = true server = "https://ntfy.sh" topic = "my-alerts" # Multipart file upload with token protection [[route]] name = "file-upload" contentType = "multipart" hcaptchaProtected = false requireToken = true ``` ### Configuration Fields - `name` - Route identifier (alphanumeric, dash, underscore only) - `contentType` - One of: `json`, `form`, `multipart`, `text`, `raw` - `hcaptchaProtected` - Enable hCaptcha verification (default: false) - `hcaptchaSecret` - hCaptcha secret key (required if hcaptchaProtected is true) - `requireToken` - Enable CSRF token protection (default: false) - `ntfy` - Optional ntfy notification configuration: - `enabled` - Enable notifications (required) - `server` - ntfy server URL (required if enabled) - `topic` - ntfy topic name (required if enabled) ### Hot Reload Posthook watches `routes.toml` for changes. When you save the file: - Valid changes are applied immediately - Invalid configuration causes the process to exit (fail-fast) - No need to restart the server for route updates ## API 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 (supports file uploads and `_redirect`) ## 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 a per-request directory: `data/{routeName}/{timestamp}_{uuid}/`: - `request.json` - full stored request (headers + parsed body + file metadata) - `files/` - uploaded files (multipart only) Example `request.json`: ```json { "timestamp": 1702834567890, "uuid": "123e4567-e89b-12d3-a456-426614174000", "routeName": "my-webhook", "method": "POST", "headers": { "content-type": "application/json" }, "body": { "your": "data" }, "files": [ { "fieldName": "attachment", "originalFilename": "invoice.pdf", "filename": "0_invoice.pdf", "contentType": "application/pdf", "size": 12345, "path": "files/0_invoice.pdf" } ] } ``` ## 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 Custom Config Location ```bash docker run -p 9000:9000 \ -v $(pwd)/my-routes.toml:/app/routes.toml \ -v $(pwd)/data:/app/data \ -e POSTHOOK_TOKEN_SECRET=your-secret-here \ posthook -- --config /app/routes.toml ``` ## Runtime Configuration ### Command Line Arguments: - `--port` - Server port (default: 9000) - `--host` - Server host (default: 0.0.0.0) - `--config` - Path to routes.toml configuration file (default: ./routes.toml) - `--data-dir` - Data storage directory (default: ./data) - `--token-secret` - HMAC secret for token signing (optional, generates random if not provided) - `--cors-origins` - Allowed CORS origins (default: `*`). Supports `*`, exact origins (`https://a.com`), and host patterns (`liz.coffee`, `*.liz.coffee`). When not `*`, only `https` origins are allowed. Responses only set `Access-Control-Allow-Origin` (preflight uses the standard allow-\* headers). ### Environment Variables: - `POSTHOOK_TOKEN_SECRET` - HMAC secret for token signing (recommended for production)