mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-23 00:21:28 +00:00
180 lines
8.9 KiB
Markdown
180 lines
8.9 KiB
Markdown
# Configuration
|
||
|
||
## Secrets
|
||
|
||
WebHooker requires several secrets to function. For local development, store them in `.dev.vars`. For production, use Cloudflare Worker Secrets.
|
||
|
||
### Required Secrets
|
||
|
||
| Variable | Description |
|
||
| ----------------------- | ----------------------------------------------- |
|
||
| `GITHUB_WEBHOOK_SECRET` | Webhook secret from your GitHub App settings |
|
||
| `GITHUB_APP_ID` | Numeric ID of your GitHub App |
|
||
| `GITHUB_PRIVATE_KEY` | App private key (PEM format, with `\n` escapes) |
|
||
| `GITHUB_CLIENT_ID` | OAuth client ID from App settings |
|
||
| `GITHUB_CLIENT_SECRET` | OAuth client secret from App settings |
|
||
| `DISCORD_TOKEN` | Discord bot token |
|
||
|
||
### Optional Secrets
|
||
|
||
| Variable | Description | Default |
|
||
| ------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------- |
|
||
| `BASE_URL` | Public URL for OAuth callbacks | `http://localhost:8787` |
|
||
| `ADMIN_USER_IDS` | Comma-separated GitHub user IDs (or logins) allowed to access the Web UI | Disabled |
|
||
| `DISCORD_GATEWAY_ENABLED` | Set to `true` to connect the Discord Gateway (bot online status); messaging works without it via REST | `false` |
|
||
|
||
## Web UI
|
||
|
||
WebHooker ships with a built-in config console at `/admin` for managing routes in the browser. It is protected by GitHub OAuth plus an admin whitelist.
|
||
|
||
### Setup
|
||
|
||
1. Configure `ADMIN_USER_IDS` with the GitHub user IDs allowed to manage routes. Logins are also accepted, e.g. `ADMIN_USER_IDS=12345,RhenCloud`. If unset, the console is disabled.
|
||
2. Open `/admin` and sign in with GitHub.
|
||
3. Only users in the whitelist receive a session cookie; everyone else gets `403`.
|
||
|
||
### Endpoints
|
||
|
||
| Endpoint | Description |
|
||
| ---------------------------------- | --------------------------------------- |
|
||
| `GET /admin` | Config console UI |
|
||
| `GET /admin/login` | Start GitHub OAuth sign-in |
|
||
| `GET /admin/logout` | Destroy session |
|
||
| `GET /admin/api/me` | Current session, scope, and groups |
|
||
| `GET /admin/api/routes` | List routes (admin only) |
|
||
| `PUT /admin/api/routes` | Replace routes (admin only) |
|
||
| `GET /admin/api/groups` | List groups (scoped to access) |
|
||
| `PUT /admin/api/groups` | Replace groups (super admin only) |
|
||
| `GET /admin/api/groups/:id/routes` | List a group's routes |
|
||
| `PUT /admin/api/groups/:id/routes` | Replace a group's routes |
|
||
| `GET /admin/api/logs` | Send logs (scoped to accessible routes) |
|
||
|
||
The console lets you add, edit, delete, and toggle routes. Saved routes are written to KV `config:routes` immediately and the config cache is invalidated so the webhook pipeline picks them up on the next run.
|
||
|
||
## Routes
|
||
|
||
Routes define which events get forwarded to which Discord channels. They are stored in Cloudflare KV under the key `config:routes` as a JSON array.
|
||
|
||
There are **no default routes** — each route must define its own target. If no routes are configured, no events are forwarded.
|
||
|
||
### Route Schema
|
||
|
||
```json
|
||
{
|
||
"id": "unique-route-id",
|
||
"name": "Human-readable name",
|
||
"enabled": true,
|
||
"groupId": "my-group",
|
||
"fallback": false,
|
||
"filters": [
|
||
{ "type": "event", "match": "push" },
|
||
{ "type": "repo", "match": "org/repo", "exclude": false }
|
||
],
|
||
"target": {
|
||
"channelId": "REQUIRED_CHANNEL_ID",
|
||
"threadId": "OPTIONAL_THREAD_ID"
|
||
}
|
||
}
|
||
```
|
||
|
||
`target.channelId` is required and used as-is; there is no fallback to a default channel.
|
||
|
||
Other route fields:
|
||
|
||
| Field | Type | Required | Description |
|
||
| ---------- | ------- | -------- | ----------------------------------------------------------------------------------------------- |
|
||
| `groupId` | string | Yes | Id of the [group](#groups) this route belongs to |
|
||
| `fallback` | boolean | No | When `true`, fires only if no non-fallback route matched the event; its own filters are ignored |
|
||
| `lang` | string | No | Message language override for this route (e.g. `en`, `zh`); defaults to the global setting |
|
||
|
||
### Custom Route Example
|
||
|
||
```json
|
||
[
|
||
{
|
||
"id": "backend-prs",
|
||
"name": "Backend PRs",
|
||
"enabled": true,
|
||
"groupId": "backend-team",
|
||
"filters": [
|
||
{ "type": "repo", "match": "myorg/backend" },
|
||
{ "type": "event", "match": "pull_request" },
|
||
{ "type": "actor", "match": "[bot]", "exclude": true }
|
||
],
|
||
"target": {
|
||
"channelId": "1234567890",
|
||
"threadId": "9876543210"
|
||
}
|
||
}
|
||
]
|
||
```
|
||
|
||
## Groups
|
||
|
||
Routes belong to groups. Groups scope admin access and can restrict which events flow into them. They are stored in Cloudflare KV under the key `config:groups` as a JSON array.
|
||
|
||
### Group Schema
|
||
|
||
```json
|
||
{
|
||
"id": "backend-team",
|
||
"name": "Backend Team",
|
||
"adminIds": ["rhencloud"],
|
||
"owners": ["myorg"]
|
||
}
|
||
```
|
||
|
||
| Field | Type | Required | Description |
|
||
| ---------- | -------- | -------- | ---------------------------------------------------------------------- |
|
||
| `id` | string | Yes | Lowercase id (`a-z0-9`, `-`); referenced by each route's `groupId` |
|
||
| `name` | string | Yes | Human-readable group name |
|
||
| `adminIds` | string[] | Yes | GitHub user IDs or logins who may manage this group's routes |
|
||
| `owners` | string[] | No | Org/user logins whose events are accepted into this group; empty = all |
|
||
|
||
### Access Model
|
||
|
||
- **Super admins** (`ADMIN_USER_IDS`) see and edit every group and all routes.
|
||
- **Group admins** (`adminIds`) only see and edit the groups they manage; submitting a route outside their groups returns `403`.
|
||
- Group admin endpoints operate on a single group at a time via `/admin/api/groups/:id/routes`; `groupId` is forced from the path parameter.
|
||
- The `owners` list restricts which event actors (sender logins) the group's routes will dispatch at all.
|
||
|
||
## Filter Types
|
||
|
||
| Type | Matches | Example |
|
||
| --------- | -------------------- | -------------------------------- |
|
||
| `event` | GitHub event name | `push`, `pull_request`, `issues` |
|
||
| `repo` | Repository full name | `org/repo` |
|
||
| `actor` | Sender login | `username`, `[bot]` |
|
||
| `action` | Event action | `opened`, `closed`, `published` |
|
||
| `branch` | Branch name | `main`, `feature/*` |
|
||
| `keyword` | Text in payload body | `deploy`, `/fix\s+\d+/` (regex) |
|
||
|
||
### Filter Behavior
|
||
|
||
- All filters in a route must match for the route to trigger (AND logic)
|
||
- Set `"exclude": true` on any filter to invert it (NOT logic)
|
||
- `keyword` filter supports regex patterns — falls back to substring match if regex is invalid
|
||
- `branch` filter works for push, pull_request, pull_request_review, pull_request_review_comment, create/delete, workflow_run, and code_scanning_alert events
|
||
|
||
### Match Values
|
||
|
||
Filters accept either a single string or an array of strings:
|
||
|
||
```json
|
||
{ "type": "event", "match": "push" }
|
||
{ "type": "event", "match": ["push", "pull_request"] }
|
||
```
|
||
|
||
## KV Storage Layout
|
||
|
||
| Key Pattern | Value | TTL |
|
||
| ------------------------ | --------------------------------------------------- | ------------------ |
|
||
| `config:routes` | JSON array of routes | Permanent |
|
||
| `config:groups` | JSON array of groups | Permanent |
|
||
| `session:{id}` | Admin session `{ userId, login }` | 7 days |
|
||
| `token:{userId}` | `{ userId, accessToken, expiresAt, refreshToken? }` | 0.9 × token expiry |
|
||
| `token-reverse:{sha256}` | User id for reverse lookup by token | 0.9 × token expiry |
|
||
| `discord-link:{userId}` | GitHub user id linked to a Discord user | Permanent |
|
||
| `state:{hex}` | `{ redirectTo, expiresAt, discordUserId? }` | 600 seconds |
|
||
| `delivery:{id}` | Webhook delivery id (dedup marker) | 300 seconds |
|
||
| `logs:send:{ts}-{hex}` | Send record | 1 hour |
|