aboutsummaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2025-12-14 22:43:24 -0800
committerElizabeth Hunt <me@liz.coffee>2025-12-14 22:43:24 -0800
commitcdb1a57614068fcfefa145bc6df45c9def7ccc6a (patch)
tree92cadbecda8658c143b7625d5925e3411976a892 /README.md
parent6d318665a08c0d4564d8de23cc39425d2c0bac49 (diff)
downloadposthook-cdb1a57614068fcfefa145bc6df45c9def7ccc6a.tar.gz
posthook-cdb1a57614068fcfefa145bc6df45c9def7ccc6a.zip
Updates
Diffstat (limited to 'README.md')
-rw-r--r--README.md114
1 files changed, 78 insertions, 36 deletions
diff --git a/README.md b/README.md
index 064fa71..a59bcc1 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@ A simple API service for receiving and storing webhook requests with dynamic rou
**⚠️ 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
@@ -40,6 +41,7 @@ Content-Type: application/json
```
With hCaptcha protection:
+
```bash
POST /admin/routes
Content-Type: application/json
@@ -53,6 +55,7 @@ Content-Type: application/json
```
With ntfy notifications:
+
```bash
POST /admin/routes
Content-Type: application/json
@@ -70,6 +73,7 @@ Content-Type: application/json
```
With CSRF token protection:
+
```bash
POST /admin/routes
Content-Type: application/json
@@ -83,6 +87,7 @@ Content-Type: application/json
```
#### List Routes
+
```bash
GET /admin/routes
```
@@ -90,17 +95,19 @@ 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
- }
+ "ok": {
+ "token": "eyJ0aW1lc3RhbXAiOjE3MDI4MzQ1Njc4OTB5...",
+ "expiresAt": 1702835467890
+ }
}
```
@@ -109,6 +116,7 @@ Response:
#### Send Webhook
Send webhooks to registered routes:
+
```bash
POST /hook/{routeName}
Content-Type: application/json
@@ -119,6 +127,7 @@ Content-Type: application/json
```
For hCaptcha-protected routes, include the token:
+
```bash
POST /hook/{routeName}
H-Captcha-Response: <token>
@@ -130,6 +139,7 @@ Content-Type: application/json
```
For form submissions with redirect (returns 303 redirect instead of JSON):
+
```bash
POST /hook/{routeName}
Content-Type: application/x-www-form-urlencoded
@@ -138,16 +148,18 @@ 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>
+ <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>
@@ -159,6 +171,7 @@ Content-Type: application/json
```
Or in form data:
+
```bash
POST /hook/{routeName}
Content-Type: application/x-www-form-urlencoded
@@ -167,6 +180,7 @@ name=John&email=john@example.com&_token=<token>
```
#### Health Check
+
```bash
GET /health
```
@@ -177,7 +191,7 @@ GET /health
- `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)
+- `multipart` - Multipart form data (supports file uploads and `_redirect`)
## CSRF Token Protection
@@ -186,18 +200,18 @@ Posthook supports stateless CSRF token protection using HMAC-signed tokens. When
### 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)
+ - 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
+ - `_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
+ - HMAC signature
+ - Route name matches
+ - Token not expired (30 seconds)
+ - Timestamp not from future
### Token Secret Configuration:
@@ -218,23 +232,27 @@ npm start # Warns and generates random secret
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>
+ <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 });
+ const response = await fetch('http://localhost:9000/hook/contact-form/token');
+ const {
+ ok: { token },
+ } = await response.json();
+ res.render('contact', { token });
});
```
@@ -243,11 +261,13 @@ app.get('/contact', async (req, res) => {
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)
@@ -264,6 +284,7 @@ When you register a route with ntfy enabled, posthook will send a notification t
- Route name
Example notification:
+
```
Title: Webhook received: my-webhook
Message: Method: POST
@@ -273,26 +294,43 @@ UUID: 123e4567-e89b-12d3-a456-426614174000
## Storage Format
-Requests are stored per-topic in `data/{routeName}/{timestamp}_{uuid}.json`:
+Requests are stored per-topic in a per-request directory: `data/{routeName}/{timestamp}_{uuid}/`:
+
+- `request.json` - full stored request (headers + parsed body + file metadata)
+- `body.json` - parsed body only (convenience)
+- `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"
- }
+ "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
@@ -301,6 +339,7 @@ npm start -- --port 9000 --host 0.0.0.0 --data-dir ./data
```
### Docker
+
```bash
docker build -t posthook .
docker run -p 9000:9000 \
@@ -330,16 +369,19 @@ docker run -p 4180:4180 \
```
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)