docs: document admin groups, OAuth redirect, and API renames

This commit is contained in:
RhenCloud 2026-08-03 00:33:18 +08:00
parent 8c8cfbf211
commit 8c9720b1b3
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
10 changed files with 324 additions and 142 deletions

View file

@ -57,16 +57,16 @@ Merges a pull request.
"owner": "org", "owner": "org",
"repo": "repo", "repo": "repo",
"pullNumber": 42, "pullNumber": 42,
"mergeMethod": "squash" "method": "squash"
} }
``` ```
| Field | Type | Required | Description | | Field | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------------------- | | ------------ | ------ | -------- | -------------------------------------------------- |
| `owner` | string | Yes | Repository owner | | `owner` | string | Yes | Repository owner |
| `repo` | string | Yes | Repository name | | `repo` | string | Yes | Repository name |
| `pullNumber` | number | Yes | Pull request number | | `pullNumber` | number | Yes | Pull request number |
| `mergeMethod` | string | No | `merge`, `squash`, or `rebase` (default: `merge`) | | `method` | string | No | `merge`, `squash`, or `rebase` (default: `squash`) |
**Response:** `200` with GitHub merge response. **Response:** `200` with GitHub merge response.
@ -88,11 +88,11 @@ Closes a pull request without merging.
} }
``` ```
| Field | Type | Required | Description | | Field | Type | Required | Description |
| ------------ | ------ | -------- | ----------------------- | | ------------ | ------ | -------- | ------------------- |
| `owner` | string | Yes | Repository owner | | `owner` | string | Yes | Repository owner |
| `repo` | string | Yes | Repository name | | `repo` | string | Yes | Repository name |
| `pullNumber` | number | Yes | Pull request number | | `pullNumber` | number | Yes | Pull request number |
**Response:** `200` with GitHub update response. **Response:** `200` with GitHub update response.
@ -111,7 +111,7 @@ Adds an emoji reaction to an issue or comment.
"owner": "org", "owner": "org",
"repo": "repo", "repo": "repo",
"issueNumber": 42, "issueNumber": 42,
"content": "rocket" "reaction": "rocket"
} }
``` ```
@ -120,7 +120,7 @@ Adds an emoji reaction to an issue or comment.
| `owner` | string | Yes | Repository owner | | `owner` | string | Yes | Repository owner |
| `repo` | string | Yes | Repository name | | `repo` | string | Yes | Repository name |
| `issueNumber` | number | Yes | Issue, PR, or comment number | | `issueNumber` | number | Yes | Issue, PR, or comment number |
| `content` | string | Yes | Reaction type (see below) | | `reaction` | string | Yes | Reaction type (see below) |
**Reaction Types:** **Reaction Types:**

View file

@ -21,9 +21,9 @@ Redirects the user to GitHub's authorization page.
**Query Parameters:** **Query Parameters:**
| Parameter | Description | | Parameter | Description |
| --------- | ---------------------------------- | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `userId` | Your application's user identifier | | `redirect` | Optional relative path to return to after sign-in (e.g. `/admin`). Must start with `/` but not `//`; any unsafe value falls back to `/`. |
**Response:** `302` redirect to GitHub OAuth authorize URL. **Response:** `302` redirect to GitHub OAuth authorize URL.
@ -42,7 +42,11 @@ GitHub redirects here after authorization. Exchanges the code for an access toke
| `code` | Authorization code | | `code` | Authorization code |
| `state` | State parameter for CSRF protection | | `state` | State parameter for CSRF protection |
**Response:** Redirects to your `BASE_URL` with a success/error indicator. **Response:**
- **Browser flow** (`Accept: text/html`): sets an admin session cookie, then redirects to the `redirect` target. Users without admin access are redirected to `/admin?error=forbidden`.
- **JSON flow**: returns `{ "userId": "...", "login": "...", "redirectTo": "..." }`.
- **Discord link flow** (started with a pending `discordUserId`): links the Discord user to this GitHub account, returning `{ "ok": true, "discordUserId": "...", "login": "..." }` — or a success page in the browser.
### Revoke Token ### Revoke Token
@ -66,12 +70,14 @@ Tokens are stored in KV with key pattern `token:{userId}`:
```json ```json
{ {
"userId": "12345",
"accessToken": "gho_...", "accessToken": "gho_...",
"expiresAt": "2025-01-01T00:00:00.000Z" "expiresAt": 1735689600000,
"refreshToken": "..."
} }
``` ```
Tokens are automatically expired based on the `expiresAt` timestamp. `expiresAt` is a Unix timestamp in milliseconds. KV entries expire at 90% of the token's lifetime (minimum 60 seconds). A reverse index `token-reverse:{sha256 of token}` maps the access token back to its user id so Bearer-authenticated endpoints can resolve the caller. Discord users linked to a GitHub account are stored under `discord-link:{discordUserId}`.
## Using Tokens ## Using Tokens

View file

@ -10,28 +10,34 @@ https://your-worker.workers.dev
## Endpoints ## Endpoints
| Method | Path | Auth | Description | | Method | Path | Auth | Description |
| -------- | ----------------------- | -------------- | ------------------------ | | -------- | ------------------------------ | -------------- | ------------------------ |
| `GET` | `/health` | None | Health check | | `GET` | `/health` | None | Health check |
| `POST` | `/webhook` | HMAC signature | GitHub webhook ingestion | | `POST` | `/webhook` | HMAC signature | GitHub webhook ingestion |
| `GET` | `/auth/github` | None | Start GitHub OAuth flow | | `GET` | `/auth/github` | None | Start GitHub OAuth flow |
| `GET` | `/auth/github/callback` | None | OAuth callback | | `GET` | `/auth/github/callback` | None | OAuth callback |
| `DELETE` | `/auth/token/:userId` | None | Revoke user token | | `DELETE` | `/auth/token/:userId` | None | Revoke user token |
| `POST` | `/api/comment` | Bearer token | Create issue comment | | `POST` | `/api/comment` | Bearer token | Create issue comment |
| `POST` | `/api/merge` | Bearer token | Merge pull request | | `POST` | `/api/merge` | Bearer token | Merge pull request |
| `POST` | `/api/close` | Bearer token | Close pull request | | `POST` | `/api/close` | Bearer token | Close pull request |
| `POST` | `/api/react` | Bearer token | Add reaction to issue | | `POST` | `/api/react` | Bearer token | Add reaction to issue |
| `GET` | `/admin` | Admin session | Config console UI | | `GET` | `/admin` | Admin session | Config console UI |
| `GET` | `/admin/api/routes` | Admin session | List routes | | `GET` | `/admin/api/routes` | Admin session | List routes |
| `PUT` | `/admin/api/routes` | Admin session | Replace routes | | `PUT` | `/admin/api/routes` | Admin session | Replace routes |
| `GET` | `/admin/api/groups` | Admin session | List groups (scoped) |
| `PUT` | `/admin/api/groups` | Admin session | Replace groups (super) |
| `GET` | `/admin/api/groups/:id/routes` | Admin session | List a group's routes |
| `PUT` | `/admin/api/groups/:id/routes` | Admin session | Replace a group's routes |
| `GET` | `/admin/api/me` | Admin session | Current session info |
| `GET` | `/admin/api/logs` | Admin session | Send logs (scoped) |
## Admin Console ## Admin Console
See [Configuration → Web UI](../guide/configuration.md#web-ui) for setup. Admin endpoints require a session cookie obtained via `GET /admin/login` (GitHub OAuth); the signed-in user must be listed in `ADMIN_USER_IDS`. See [Configuration → Web UI](../guide/configuration.md#web-ui) for setup. Admin endpoints require a session cookie obtained via `GET /admin/login` (GitHub OAuth); the signed-in user must be listed in `ADMIN_USER_IDS` or manage a group.
- `GET /admin` — Serves the config console HTML - `GET /admin` — Serves the config console HTML
- `GET /admin/api/routes` — Returns `{ "routes": Route[] }` - `GET /admin/api/routes` — Returns `{ "routes": Route[] }`
- `PUT /admin/api/routes` — Body `{ "routes": Route[] }`; validates each route (id pattern, unique id, name, enabled, ≥1 valid filter, string `target.channelId`) and persists to KV `config:routes`. Returns `200 { ok, count }` or `400 { error }` / `401 { error }`. - `PUT /admin/api/routes` — Body `{ "routes": Route[] }`; validates each route (id pattern, unique id, name, enabled, `groupId`, filters — empty only allowed for `fallback` routes — and string `target.channelId`) and persists to KV `config:routes`. Returns `200 { ok, count }` or `400 { error }` / `401 { error }`.
## Health Check ## Health Check

View file

@ -16,16 +16,24 @@ npm run dev # Start local dev server
src/ src/
├── index.ts # CF Workers entry (fetch + scheduled), exports DiscordGateway DO ├── index.ts # CF Workers entry (fetch + scheduled), exports DiscordGateway DO
├── types.ts # Env, Config, Route, Filter, WebhookEvent, FormattedMessage ├── types.ts # Env, Config, Route, Filter, WebhookEvent, FormattedMessage
├── config.ts # Loads routes from KV (fallback to 7 defaults), builds Config from env ├── config.ts # Loads routes from KV (returns [] if unset), builds Config from env
├── server.ts # Hono app: /health, /webhook, mounts /auth + / ├── server.ts # Hono app: /health, /webhook, mounts /auth, /admin + /
├── webhook.ts # HMAC verify (Web Crypto), parseEvent, extractBranch, matchRoute ├── webhook.ts # HMAC verify (Web Crypto), parseEvent, extractBranch, matchRoute
├── discord.ts # Dispatch via DO RPC, initGateway (scheduled) ├── discord.ts # Dispatch via Discord REST (DO RPC when gateway enabled), initGateway (scheduled)
├── discord-rest.ts # Discord REST sendMessage with retry + rate-limit handling
├── discord-gateway.ts # Durable Object: Discord Gateway WS, heartbeat, channel cache, send ├── discord-gateway.ts # Durable Object: Discord Gateway WS, heartbeat, channel cache, send
├── formatter.ts # 23 event formatters + generic fallback ├── formatter.ts # 23 event formatters + generic fallback
├── github-oauth.ts # OAuth URL, callback token exchange, getUserOctokit ├── github-oauth.ts # OAuth URL, callback token exchange, getUserOctokit
├── oauth-routes.ts # GET /auth/github, callback, DELETE /token/:userId (KV state) ├── oauth-routes.ts # GET /auth/github, callback, DELETE /token/:userId (KV state)
├── action-routes.ts # POST /api/comment|merge|react (Bearer token auth via KV lookup) ├── action-routes.ts # POST /api/comment|merge|close|react (Bearer token auth via KV lookup)
├── admin-routes.ts # /admin API: routes, groups, me, logs (session + scope auth)
├── admin-session.ts # Admin session CRUD (KV session:{id}), cookie helpers
├── groups.ts # Group loading, group-admin access scoping
├── i18n.ts # Message language overrides (en/zh)
├── send-log.ts # Send logging (logs:send KV keys)
├── token-store.ts # KV-based token CRUD with findUserIdByToken reverse lookup ├── token-store.ts # KV-based token CRUD with findUserIdByToken reverse lookup
├── home-routes.ts # Landing page routes
├── legal-routes.ts # Legal page routes
└── log.ts # JSON console logger (info/warn/error/fatal) └── log.ts # JSON console logger (info/warn/error/fatal)
``` ```
@ -37,6 +45,7 @@ src/
| `npm run typecheck` | TypeScript type checking | | `npm run typecheck` | TypeScript type checking |
| `npm run lint` | ESLint (TypeScript) | | `npm run lint` | ESLint (TypeScript) |
| `npm run lint:md` | Markdownlint (Markdown) | | `npm run lint:md` | Markdownlint (Markdown) |
| `npm test` | Run unit tests (bun test) |
| `npm run format` | Format all files with Prettier | | `npm run format` | Format all files with Prettier |
| `npm run format:check` | Check Prettier formatting | | `npm run format:check` | Check Prettier formatting |
| `npm run docs:dev` | Start VitePress docs dev server | | `npm run docs:dev` | Start VitePress docs dev server |
@ -56,10 +65,10 @@ src/
## Testing ## Testing
```bash ```bash
# Functional tests (requires wrangler dev running) # Run the unit test suite (bun test)
bash /tmp/test-webhooker.sh npm test
# Or manually # Or manually check the health endpoint
curl http://localhost:8787/health curl http://localhost:8787/health
``` ```

View file

@ -35,13 +35,19 @@ WebHooker ships with a built-in config console at `/admin` for managing routes i
### Endpoints ### Endpoints
| Endpoint | Description | | Endpoint | Description |
| ----------------------- | --------------------------- | | ---------------------------------- | --------------------------------------- |
| `GET /admin` | Config console UI | | `GET /admin` | Config console UI |
| `GET /admin/login` | Start GitHub OAuth sign-in | | `GET /admin/login` | Start GitHub OAuth sign-in |
| `GET /admin/logout` | Destroy session | | `GET /admin/logout` | Destroy session |
| `GET /admin/api/routes` | List routes (admin only) | | `GET /admin/api/me` | Current session, scope, and groups |
| `PUT /admin/api/routes` | Replace routes (admin only) | | `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. 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.
@ -58,6 +64,8 @@ There are **no default routes** — each route must define its own target. If no
"id": "unique-route-id", "id": "unique-route-id",
"name": "Human-readable name", "name": "Human-readable name",
"enabled": true, "enabled": true,
"groupId": "my-group",
"fallback": false,
"filters": [ "filters": [
{ "type": "event", "match": "push" }, { "type": "event", "match": "push" },
{ "type": "repo", "match": "org/repo", "exclude": false } { "type": "repo", "match": "org/repo", "exclude": false }
@ -71,6 +79,14 @@ There are **no default routes** — each route must define its own target. If no
`target.channelId` is required and used as-is; there is no fallback to a default channel. `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 ### Custom Route Example
```json ```json
@ -79,6 +95,7 @@ There are **no default routes** — each route must define its own target. If no
"id": "backend-prs", "id": "backend-prs",
"name": "Backend PRs", "name": "Backend PRs",
"enabled": true, "enabled": true,
"groupId": "backend-team",
"filters": [ "filters": [
{ "type": "repo", "match": "myorg/backend" }, { "type": "repo", "match": "myorg/backend" },
{ "type": "event", "match": "pull_request" }, { "type": "event", "match": "pull_request" },
@ -92,6 +109,35 @@ There are **no default routes** — each route must define its own target. If no
] ]
``` ```
## 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 ## Filter Types
| Type | Matches | Example | | Type | Matches | Example |
@ -108,7 +154,7 @@ There are **no default routes** — each route must define its own target. If no
- All filters in a route must match for the route to trigger (AND logic) - 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) - 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 - `keyword` filter supports regex patterns — falls back to substring match if regex is invalid
- `branch` filter works for push, pull_request, create/delete, workflow_run, and code_scanning_alert events - `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 ### Match Values
@ -121,9 +167,14 @@ Filters accept either a single string or an array of strings:
## KV Storage Layout ## KV Storage Layout
| Key Pattern | Value | TTL | | Key Pattern | Value | TTL |
| ---------------- | --------------------------------- | ------------ | | ------------------------ | --------------------------------------------------- | ------------------ |
| `config:routes` | JSON array of routes | Permanent | | `config:routes` | JSON array of routes | Permanent |
| `session:{id}` | Admin session `{ userId, login }` | 7 days | | `config:groups` | JSON array of groups | Permanent |
| `token:{userId}` | `{ accessToken, expiresAt }` | Until expiry | | `session:{id}` | Admin session `{ userId, login }` | 7 days |
| `state:{hex}` | `{ userId, createdAt }` | 600 seconds | | `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 |

View file

@ -33,12 +33,12 @@ POST /api/comment
} }
``` ```
| 字段 | 类型 | 必需 | 说明 | | 字段 | 类型 | 必需 | 说明 |
| --- | --- | --- | --- | | ------------- | ------ | ---- | ------------------------- |
| `owner` | string | 是 | 仓库所有者 | | `owner` | string | 是 | 仓库所有者 |
| `repo` | string | 是 | 仓库名称 | | `repo` | string | 是 | 仓库名称 |
| `issueNumber` | number | 是 | 议题或 PR 编号 | | `issueNumber` | number | 是 | 议题或 PR 编号 |
| `body` | string | 是 | 评论内容(支持 Markdown | | `body` | string | 是 | 评论内容(支持 Markdown |
**响应:** `200` 与 GitHub API 响应。 **响应:** `200` 与 GitHub API 响应。
@ -57,19 +57,45 @@ POST /api/merge
"owner": "org", "owner": "org",
"repo": "repo", "repo": "repo",
"pullNumber": 42, "pullNumber": 42,
"mergeMethod": "squash" "method": "squash"
} }
``` ```
| 字段 | 类型 | 必需 | 说明 | | 字段 | 类型 | 必需 | 说明 |
| --- | --- | --- | --- | | ------------ | ------ | ---- | ----------------------------------------------- |
| `owner` | string | 是 | 仓库所有者 | | `owner` | string | 是 | 仓库所有者 |
| `repo` | string | 是 | 仓库名称 | | `repo` | string | 是 | 仓库名称 |
| `pullNumber` | number | 是 | 拉取请求编号 | | `pullNumber` | number | 是 | 拉取请求编号 |
| `mergeMethod` | string | 否 | `merge``squash``rebase`(默认:`merge` | | `method` | string | 否 | `merge``squash``rebase`(默认:`squash` |
**响应:** `200` 与 GitHub 合并响应。 **响应:** `200` 与 GitHub 合并响应。
### 关闭拉取请求
```
POST /api/close
```
不合并、直接关闭拉取请求。
**请求体:**
```json
{
"owner": "org",
"repo": "repo",
"pullNumber": 42
}
```
| 字段 | 类型 | 必需 | 说明 |
| ------------ | ------ | ---- | ------------ |
| `owner` | string | 是 | 仓库所有者 |
| `repo` | string | 是 | 仓库名称 |
| `pullNumber` | number | 是 | 拉取请求编号 |
**响应:** `200` 与 GitHub 更新响应。
### 添加反应 ### 添加反应
``` ```
@ -85,16 +111,16 @@ POST /api/react
"owner": "org", "owner": "org",
"repo": "repo", "repo": "repo",
"issueNumber": 42, "issueNumber": 42,
"content": "rocket" "reaction": "rocket"
} }
``` ```
| 字段 | 类型 | 必需 | 说明 | | 字段 | 类型 | 必需 | 说明 |
| --- | --- | --- | --- | | ------------- | ------ | ---- | ------------------- |
| `owner` | string | 是 | 仓库所有者 | | `owner` | string | 是 | 仓库所有者 |
| `repo` | string | 是 | 仓库名称 | | `repo` | string | 是 | 仓库名称 |
| `issueNumber` | number | 是 | 议题、PR 或评论编号 | | `issueNumber` | number | 是 | 议题、PR 或评论编号 |
| `content` | string | 是 | 反应类型(见下方) | | `reaction` | string | 是 | 反应类型(见下方) |
**反应类型:** **反应类型:**
@ -104,8 +130,8 @@ POST /api/react
## 错误响应 ## 错误响应
| 状态码 | 响应体 | 原因 | | 状态码 | 响应体 | 原因 |
| --- | --- | --- | | ------ | --------------------------- | ------------------------- |
| `401` | `{"error": "Unauthorized"}` | 缺少或无效的 Bearer Token | | `401` | `{"error": "Unauthorized"}` | 缺少或无效的 Bearer Token |
| `400` | `{"error": "..."}` | 无效的请求体 | | `400` | `{"error": "..."}` | 无效的请求体 |
| `500` | `{"error": "..."}` | GitHub API 错误 | | `500` | `{"error": "..."}` | GitHub API 错误 |

View file

@ -21,9 +21,9 @@ GET /auth/github
**查询参数:** **查询参数:**
| 参数 | 说明 | | 参数 | 说明 |
| --- | --- | | ---------- | -------------------------------------------------------------------------------------------------------- |
| `userId` | 你的应用用户标识符 | | `redirect` | 可选,登录后返回的相对路径(如 `/admin`)。必须以 `/` 开头但不能以 `//` 开头;任何不安全的值回退为 `/` |
**响应:** `302` 重定向到 GitHub OAuth 授权 URL。 **响应:** `302` 重定向到 GitHub OAuth 授权 URL。
@ -37,12 +37,16 @@ GitHub 授权后重定向到此地址。将 code 交换为访问令牌并存储
**查询参数(来自 GitHub** **查询参数(来自 GitHub**
| 参数 | 说明 | | 参数 | 说明 |
| --- | --- | | ------- | ------------------- |
| `code` | 授权码 | | `code` | 授权码 |
| `state` | CSRF 保护的状态参数 | | `state` | CSRF 保护的状态参数 |
**响应:** 重定向到你的 `BASE_URL`,附带成功/失败指示。 **响应:**
- **浏览器流程**`Accept: text/html`):设置管理员会话 Cookie然后重定向到 `redirect` 目标;无管理权限的用户被重定向到 `/admin?error=forbidden`
- **JSON 流程**:返回 `{ "userId": "...", "login": "...", "redirectTo": "..." }`
- **Discord 绑定流程**(以未决的 `discordUserId` 启动时):将 Discord 用户绑定到此 GitHub 账号,返回 `{ "ok": true, "discordUserId": "...", "login": "..." }`——浏览器中则显示成功页面。
### 撤销 Token ### 撤销 Token
@ -66,12 +70,14 @@ Token 以键模式 `token:{userId}` 存储在 KV 中:
```json ```json
{ {
"userId": "12345",
"accessToken": "gho_...", "accessToken": "gho_...",
"expiresAt": "2025-01-01T00:00:00.000Z" "expiresAt": 1735689600000,
"refreshToken": "..."
} }
``` ```
Token 会根据 `expiresAt` 时间戳自动过期 `expiresAt` 是毫秒级 Unix 时间戳。KV 条目在 Token 有效期的 90% 时过期(至少 60 秒)。反向索引 `token-reverse:{sha256 of token}` 将访问令牌映射回用户 id使 Bearer 鉴权的端点能解析调用者。与 GitHub 账号绑定的 Discord 用户存储在 `discord-link:{discordUserId}`
## 使用 Token ## 使用 Token

View file

@ -10,16 +10,34 @@ https://your-worker.workers.dev
## 端点 ## 端点
| 方法 | 路径 | 鉴权 | 说明 | | 方法 | 路径 | 鉴权 | 说明 |
| --- | --- | --- | --- | | -------- | ------------------------------ | ------------ | ------------------------ |
| `GET` | `/health` | 无 | 健康检查 | | `GET` | `/health` | 无 | 健康检查 |
| `POST` | `/webhook` | HMAC 签名 | GitHub webhook 接入 | | `POST` | `/webhook` | HMAC 签名 | GitHub webhook 接入 |
| `GET` | `/auth/github` | 无 | 启动 GitHub OAuth 流程 | | `GET` | `/auth/github` | 无 | 启动 GitHub OAuth 流程 |
| `GET` | `/auth/github/callback` | 无 | OAuth 回调 | | `GET` | `/auth/github/callback` | 无 | OAuth 回调 |
| `DELETE` | `/auth/token/:userId` | 无 | 撤销用户 Token | | `DELETE` | `/auth/token/:userId` | 无 | 撤销用户 Token |
| `POST` | `/api/comment` | Bearer Token | 创建议题评论 | | `POST` | `/api/comment` | Bearer Token | 创建议题评论 |
| `POST` | `/api/merge` | Bearer Token | 合并拉取请求 | | `POST` | `/api/merge` | Bearer Token | 合并拉取请求 |
| `POST` | `/api/react` | Bearer Token | 添加议题反应 | | `POST` | `/api/close` | Bearer Token | 关闭拉取请求 |
| `POST` | `/api/react` | Bearer Token | 添加议题反应 |
| `GET` | `/admin` | 管理员会话 | 配置控制台页面 |
| `GET` | `/admin/api/routes` | 管理员会话 | 列出路由 |
| `PUT` | `/admin/api/routes` | 管理员会话 | 替换路由 |
| `GET` | `/admin/api/groups` | 管理员会话 | 列出分组(按权限过滤) |
| `PUT` | `/admin/api/groups` | 管理员会话 | 替换分组(仅超级管理员) |
| `GET` | `/admin/api/groups/:id/routes` | 管理员会话 | 列出某分组的路由 |
| `PUT` | `/admin/api/groups/:id/routes` | 管理员会话 | 替换某分组的路由 |
| `GET` | `/admin/api/me` | 管理员会话 | 当前会话信息 |
| `GET` | `/admin/api/logs` | 管理员会话 | 发送日志(按权限过滤) |
## 管理控制台
参见[配置 → Web 控制台](../guide/configuration.md#web-ui)了解设置方法。管理端点需要会话 Cookie可通过 `GET /admin/login`GitHub OAuth获取登录用户必须列在 `ADMIN_USER_IDS` 中。
- `GET /admin` — 提供配置控制台 HTML
- `GET /admin/api/routes` — 返回 `{ "routes": Route[] }`
- `PUT /admin/api/routes` — 请求体为 `{ "routes": Route[] }`校验每条路由id 格式、唯一 id、name、enabled、groupId、过滤器、字符串 `target.channelId`)并持久化到 KV `config:routes`。返回 `200 { ok, count }``400 { error }` / `401 { error }`
## 健康检查 ## 健康检查
@ -45,11 +63,11 @@ POST /webhook
**请求头:** **请求头:**
| 头部 | 必需 | 说明 | | 头部 | 必需 | 说明 |
| --- | --- | --- | | --------------------- | ---- | ---------------- |
| `X-Hub-Signature-256` | 是 | HMAC-SHA256 签名 | | `X-Hub-Signature-256` | 是 | HMAC-SHA256 签名 |
| `X-GitHub-Event` | 是 | 事件类型名称 | | `X-GitHub-Event` | 是 | 事件类型名称 |
| `X-GitHub-Delivery` | 是 | 唯一投递 ID | | `X-GitHub-Delivery` | 是 | 唯一投递 ID |
**请求体:** GitHub webhook JSON 载荷(最大 1MB **请求体:** GitHub webhook JSON 载荷(最大 1MB
@ -63,11 +81,11 @@ POST /webhook
**错误响应:** **错误响应:**
| 状态码 | 响应体 | 原因 | | 状态码 | 响应体 | 原因 |
| --- | --- | --- | | ------ | -------------------------------- | ---------------------------- |
| `401` | `{"error": "Invalid signature"}` | 签名验证失败 | | `401` | `{"error": "Invalid signature"}` | 签名验证失败 |
| `400` | `{"error": "Invalid event"}` | 缺少事件头或格式错误的请求体 | | `400` | `{"error": "Invalid event"}` | 缺少事件头或格式错误的请求体 |
| `413` | `{"error": "Request too large"}` | 请求体超过 1MB 限制 | | `413` | `{"error": "Request too large"}` | 请求体超过 1MB 限制 |
## 错误格式 ## 错误格式

View file

@ -16,31 +16,40 @@ npm run dev # 启动本地开发服务器
src/ src/
├── index.ts # CF Workers 入口 (fetch + scheduled),导出 DiscordGateway DO ├── index.ts # CF Workers 入口 (fetch + scheduled),导出 DiscordGateway DO
├── types.ts # Env、Config、Route、Filter、WebhookEvent、FormattedMessage ├── types.ts # Env、Config、Route、Filter、WebhookEvent、FormattedMessage
├── config.ts # 从 KV 加载路由(回退到 7 条默认),从 env 构建 Config ├── config.ts # 从 KV 加载路由(未设置时返回 []),从 env 构建 Config
├── server.ts # Hono 应用: /health、/webhook挂载 /auth + / ├── server.ts # Hono 应用: /health、/webhook挂载 /auth、/admin + /
├── webhook.ts # HMAC 验证 (Web Crypto)、parseEvent、extractBranch、matchRoute ├── webhook.ts # HMAC 验证 (Web Crypto)、parseEvent、extractBranch、matchRoute
├── discord.ts # 通过 DO RPC 分发、initGateway (scheduled) ├── discord.ts # 通过 Discord REST 分发(启用 Gateway 时走 DO RPC、initGateway (scheduled)
├── discord-rest.ts # Discord REST sendMessage带重试和限流处理
├── discord-gateway.ts # Durable Object: Discord Gateway WS、心跳、频道缓存、发送 ├── discord-gateway.ts # Durable Object: Discord Gateway WS、心跳、频道缓存、发送
├── formatter.ts # 23 种事件格式化器 + 通用回退 ├── formatter.ts # 23 种事件格式化器 + 通用回退
├── github-oauth.ts # OAuth URL、回调 Token 交换、getUserOctokit ├── github-oauth.ts # OAuth URL、回调 Token 交换、getUserOctokit
├── oauth-routes.ts # GET /auth/github、回调、DELETE /token/:userId (KV 状态) ├── oauth-routes.ts # GET /auth/github、回调、DELETE /token/:userId (KV 状态)
├── action-routes.ts # POST /api/comment|merge|react (通过 KV 查找进行 Bearer Token 鉴权) ├── action-routes.ts # POST /api/comment|merge|close|react (通过 KV 查找进行 Bearer Token 鉴权)
├── admin-routes.ts # /admin API路由、分组、me、日志会话 + 权限范围鉴权)
├── admin-session.ts # 管理员会话 CRUD (KV session:{id})、Cookie 辅助函数
├── groups.ts # 分组加载、分组管理员权限范围
├── i18n.ts # 消息语言覆盖 (en/zh)
├── send-log.ts # 发送日志 (logs:send KV 键)
├── token-store.ts # 基于 KV 的 Token CRUD带 findUserIdByToken 反向查找 ├── token-store.ts # 基于 KV 的 Token CRUD带 findUserIdByToken 反向查找
├── home-routes.ts # 落地页路由
├── legal-routes.ts # 法律页面路由
└── log.ts # JSON 控制台日志 (info/warn/error/fatal) └── log.ts # JSON 控制台日志 (info/warn/error/fatal)
``` ```
## 脚本 ## 脚本
| 命令 | 说明 | | 命令 | 说明 |
| --- | --- | | ---------------------- | ----------------------------- |
| `npm run dev` | 启动 wrangler dev 服务器 | | `npm run dev` | 启动 wrangler dev 服务器 |
| `npm run typecheck` | TypeScript 类型检查 | | `npm run typecheck` | TypeScript 类型检查 |
| `npm run lint` | ESLint (TypeScript) | | `npm run lint` | ESLint (TypeScript) |
| `npm run lint:md` | Markdownlint (Markdown) | | `npm run lint:md` | Markdownlint (Markdown) |
| `npm run format` | 使用 Prettier 格式化所有文件 | | `npm test` | 运行单元测试 (bun test) |
| `npm run format:check` | 检查 Prettier 格式 | | `npm run format` | 使用 Prettier 格式化所有文件 |
| `npm run docs:dev` | 启动 VitePress 文档开发服务器 | | `npm run format:check` | 检查 Prettier 格式 |
| `npm run docs:build` | 构建文档站点 | | `npm run docs:dev` | 启动 VitePress 文档开发服务器 |
| `npm run docs:build` | 构建文档站点 |
## 代码风格 ## 代码风格
@ -56,10 +65,10 @@ src/
## 测试 ## 测试
```bash ```bash
# 功能测试(需要正在运行的 wrangler dev # 运行单元测试套件 (bun test)
bash /tmp/test-webhooker.sh npm test
# 或手动 # 或手动检查健康端点
curl http://localhost:8787/health curl http://localhost:8787/health
``` ```

View file

@ -35,13 +35,19 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
### 端点 ### 端点
| 端点 | 说明 | | 端点 | 说明 |
| ----------------------- | ---------------------- | | ---------------------------------- | ---------------------------- |
| `GET /admin` | 配置控制台页面 | | `GET /admin` | 配置控制台页面 |
| `GET /admin/login` | 开始 GitHub OAuth 登录 | | `GET /admin/login` | 开始 GitHub OAuth 登录 |
| `GET /admin/logout` | 销毁会话 | | `GET /admin/logout` | 销毁会话 |
| `GET /admin/api/routes` | 列出路由(仅管理员) | | `GET /admin/api/me` | 当前会话、权限范围和分组 |
| `PUT /admin/api/routes` | 替换路由(仅管理员) | | `GET /admin/api/routes` | 列出路由(仅管理员) |
| `PUT /admin/api/routes` | 替换路由(仅管理员) |
| `GET /admin/api/groups` | 列出分组(按权限过滤) |
| `PUT /admin/api/groups` | 替换分组(仅超级管理员) |
| `GET /admin/api/groups/:id/routes` | 列出某分组的路由 |
| `PUT /admin/api/groups/:id/routes` | 替换某分组的路由 |
| `GET /admin/api/logs` | 发送日志(按可访问路由过滤) |
控制台支持新增、编辑、删除和开关路由。保存后立即写入 KV `config:routes` 并使配置缓存失效,下一次 webhook 处理即会生效。 控制台支持新增、编辑、删除和开关路由。保存后立即写入 KV `config:routes` 并使配置缓存失效,下一次 webhook 处理即会生效。
@ -58,6 +64,8 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
"id": "unique-route-id", "id": "unique-route-id",
"name": "可读名称", "name": "可读名称",
"enabled": true, "enabled": true,
"groupId": "my-group",
"fallback": false,
"filters": [ "filters": [
{ "type": "event", "match": "push" }, { "type": "event", "match": "push" },
{ "type": "repo", "match": "org/repo", "exclude": false } { "type": "repo", "match": "org/repo", "exclude": false }
@ -71,6 +79,14 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
`target.channelId` 必填且按原样使用,不存在默认频道回退。 `target.channelId` 必填且按原样使用,不存在默认频道回退。
其他路由字段:
| 字段 | 类型 | 必需 | 说明 |
| ---------- | ------- | ---- | ---------------------------------------------------------------------- |
| `groupId` | string | 是 | 该路由所属[分组](#分组)的 id |
| `fallback` | boolean | 否 | 为 `true` 时,仅当没有其它路由匹配该事件时才发送,其自身过滤器会被忽略 |
| `lang` | string | 否 | 该路由的消息语言覆盖(如 `en``zh`),默认跟随全局设置 |
### 自定义路由示例 ### 自定义路由示例
```json ```json
@ -79,6 +95,7 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
"id": "backend-prs", "id": "backend-prs",
"name": "后端 PR", "name": "后端 PR",
"enabled": true, "enabled": true,
"groupId": "backend-team",
"filters": [ "filters": [
{ "type": "repo", "match": "myorg/backend" }, { "type": "repo", "match": "myorg/backend" },
{ "type": "event", "match": "pull_request" }, { "type": "event", "match": "pull_request" },
@ -92,6 +109,35 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
] ]
``` ```
## 分组
路由隶属于分组。分组用于限定管理权限,并可限制哪些事件允许流入。它们以 JSON 数组形式存储在 Cloudflare KV 中,键为 `config:groups`
### 分组模式
```json
{
"id": "backend-team",
"name": "后端团队",
"adminIds": ["rhencloud"],
"owners": ["myorg"]
}
```
| 字段 | 类型 | 必需 | 说明 |
| ---------- | -------- | ---- | ----------------------------------------------------- |
| `id` | string | 是 | 小写 id`a-z0-9``-`),由每条路由的 `groupId` 引用 |
| `name` | string | 是 | 可读的分组名称 |
| `adminIds` | string[] | 是 | 可管理该分组路由的 GitHub 用户 ID 或登录名 |
| `owners` | string[] | 否 | 允许事件进入该分组的组织/用户登录名;为空表示不限制 |
### 权限模型
- **超级管理员**`ADMIN_USER_IDS`)可查看和编辑所有分组及全部路由。
- **分组管理员**`adminIds`)只能查看和编辑其管理的分组;提交其分组之外的路由返回 `403`
- 分组管理端点通过 `/admin/api/groups/:id/routes` 一次只操作一个分组;`groupId` 由路径参数强制指定。
- `owners` 列表限定哪些事件参与者(发送者登录名)的事件会被该分组的路由投递。
## 过滤器类型 ## 过滤器类型
| 类型 | 匹配对象 | 示例 | | 类型 | 匹配对象 | 示例 |
@ -108,7 +154,7 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
- 路由中的所有过滤器必须都匹配才触发路由AND 逻辑) - 路由中的所有过滤器必须都匹配才触发路由AND 逻辑)
- 在任何过滤器上设置 `"exclude": true` 可反转匹配逻辑NOT 逻辑) - 在任何过滤器上设置 `"exclude": true` 可反转匹配逻辑NOT 逻辑)
- `keyword` 过滤器支持正则表达式——如果正则有误,回退到子串匹配 - `keyword` 过滤器支持正则表达式——如果正则有误,回退到子串匹配
- `branch` 过滤器适用于 push、pull_request、create/delete、workflow_run 和 code_scanning_alert 事件 - `branch` 过滤器适用于 push、pull_request、pull_request_review、pull_request_review_comment、create/delete、workflow_run 和 code_scanning_alert 事件
### 匹配值 ### 匹配值
@ -121,9 +167,14 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
## KV 存储布局 ## KV 存储布局
| 键模式 | 值 | TTL | | 键模式 | 值 | TTL |
| ---------------- | ------------------------------ | ------ | | ------------------------ | --------------------------------------------------- | ------------------ |
| `config:routes` | JSON 路由数组 | 永久 | | `config:routes` | JSON 路由数组 | 永久 |
| `session:{id}` | 管理员会话 `{ userId, login }` | 7 天 | | `config:groups` | JSON 分组数组 | 永久 |
| `token:{userId}` | `{ accessToken, expiresAt }` | 至过期 | | `session:{id}` | 管理员会话 `{ userId, login }` | 7 天 |
| `state:{hex}` | `{ userId, createdAt }` | 600 秒 | | `token:{userId}` | `{ userId, accessToken, expiresAt, refreshToken? }` | 0.9 × Token 有效期 |
| `token-reverse:{sha256}` | 用于按 Token 反查的用户 id | 0.9 × Token 有效期 |
| `discord-link:{userId}` | 与 Discord 用户绑定的 GitHub 用户 id | 永久 |
| `state:{hex}` | `{ redirectTo, expiresAt, discordUserId? }` | 600 秒 |
| `delivery:{id}` | Webhook 投递 id去重标记 | 300 秒 |
| `logs:send:{ts}-{hex}` | 发送记录 | 1 小时 |