aboutsummaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2025-12-14 20:36:24 -0800
committerElizabeth Hunt <me@liz.coffee>2025-12-14 20:36:24 -0800
commit6bf57766feb8321f860baf300140563cd9539053 (patch)
treed80ff78c2a7f4dbea79f9ee850542aee1b735ef4 /README.md
downloadposthook-6bf57766feb8321f860baf300140563cd9539053.tar.gz
posthook-6bf57766feb8321f860baf300140563cd9539053.zip
Init
Diffstat (limited to 'README.md')
-rw-r--r--README.md345
1 files changed, 345 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..064fa71
--- /dev/null
+++ b/README.md
@@ -0,0 +1,345 @@
+# 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: <token>
+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
+<form action="/hook/my-form" method="POST">
+ <input type="text" name="name" required>
+ <input type="email" name="email" required>
+ <input type="hidden" name="_redirect" value="https://example.com/thank-you">
+ <button type="submit">Submit</button>
+</form>
+```
+
+For token-protected routes, include the token:
+```bash
+POST /hook/{routeName}
+X-CSRF-Token: <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=<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 fetches the token before rendering -->
+<form action="/hook/contact-form" method="POST">
+ <input type="hidden" name="_token" value="<%= token %>">
+ <input type="text" name="name" required>
+ <input type="email" name="email" required>
+ <input type="hidden" name="_redirect" value="/thanks">
+ <button type="submit">Submit</button>
+</form>
+```
+
+**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)