docs: document roles, invites, audit log and self-signup

- configuration.md (en/zh): ALLOW_SELF_SIGNUP, AUDIT_RETENTION_DAYS,
  member schema, role table, invite/self-signup sections, invite KV key,
  audit_logs D1 table, new admin endpoints
- deployment.md (en/zh): migration list through 0005
- README (en/zh) and AGENTS.md: access model, invites, audit log
- config.example.yaml: members example; .env.example: new variables
- .gitignore: ignore local saas-roadmap.md
This commit is contained in:
RhenCloud 2026-08-11 23:38:50 +08:00
parent 11b74aa82a
commit ce9f6147f1
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
10 changed files with 184 additions and 76 deletions

View file

@ -20,6 +20,8 @@ TELEGRAM_WEBHOOK_SECRET=your-webhook-secret
# Admin # Admin
ADMIN_USER_IDS=your-github-id,your-github-login ADMIN_USER_IDS=your-github-id,your-github-login
# ALLOW_SELF_SIGNUP=1 # non-admin GitHub users get a personal group on first login (default off)
# AUDIT_RETENTION_DAYS=90 # audit log retention for the scheduled cleanup (default 90)
# Server / public URL (OAuth callbacks + Telegram webhook sync) # Server / public URL (OAuth callbacks + Telegram webhook sync)
BASE_URL=http://localhost:8787 BASE_URL=http://localhost:8787

1
.gitignore vendored
View file

@ -14,3 +14,4 @@ data/
config.yaml config.yaml
.direnv/ .direnv/
result result
saas-roadmap.md

View file

@ -16,6 +16,9 @@ Core pipeline: GitHub Webhook → Worker (verify + filter + format) → Discord
- Webhook providers: pluggable forge adapters under `src/providers/` (github, gitea) — each verifies its own signature format and normalizes its payload to a GitHub-shaped `WebhookEvent`; GitLab etc. can be added later - Webhook providers: pluggable forge adapters under `src/providers/` (github, gitea) — each verifies its own signature format and normalizes its payload to a GitHub-shaped `WebhookEvent`; GitLab etc. can be added later
- GitHub OAuth: octokit (token is stored hashed for reverse lookup) - GitHub OAuth: octokit (token is stored hashed for reverse lookup)
- Admin WebUI: `/admin` config console, OAuth-session protected via `ADMIN_USER_IDS` whitelist - Admin WebUI: `/admin` config console, OAuth-session protected via `ADMIN_USER_IDS` whitelist
- Access control: every group has role-based members (`owner` / `admin` / `viewer`); super admins bypass; legacy `adminIds` are read as owners (backward compatible); owners manage members + invites; `owners` field stays super-only
- Invites: single-use 7-day links (`invite:{token}`) for joining a group as admin/viewer; `ALLOW_SELF_SIGNUP=1` gives access-less users a personal group on first login (self-service SaaS entry)
- Audit log: every admin operation (logins, group/route/member/invite changes) recorded in D1 `audit_logs`; pruned by the scheduled trigger after `AUDIT_RETENTION_DAYS` (default 90)
- Local dev: wrangler + Miniflare - Local dev: wrangler + Miniflare
## Architecture ## Architecture
@ -66,21 +69,24 @@ src/
│ ├── oauth.ts # OAuth URL, callback token exchange, getUserOctokit, comment/getComment/editComment/deleteComment/merge/close actions │ ├── oauth.ts # OAuth URL, callback token exchange, getUserOctokit, comment/getComment/editComment/deleteComment/merge/close actions
│ └── store.ts # KV token CRUD + D1 discord-link/telegram-link mapping (was token-store.ts) │ └── store.ts # KV token CRUD + D1 discord-link/telegram-link mapping (was token-store.ts)
├── web/ # HTTP UI/API routes ├── web/ # HTTP UI/API routes
│ ├── oauth-routes.ts # GET /auth/github, callback (admin session / discord-link / telegram-link), DELETE /token/:userId │ ├── oauth-routes.ts # GET /auth/github, callback (admin session / invite accept / self-signup / discord-link / telegram-link), DELETE /token/:userId
│ ├── action-routes.ts # POST /api/comment|merge|close|react (Bearer token auth via KV lookup) │ ├── action-routes.ts # POST /api/comment|merge|close|react (Bearer token auth via shared middleware)
│ ├── admin-routes.ts # /admin UI + GET/PUT /admin/api/routes|groups|me|logs (session + scope auth, validation) │ ├── admin-routes.ts # /admin UI + GET/PUT /admin/api/routes|groups|me|logs|invites|audit (auth middleware + role guards, validation)
│ ├── auth.ts # Shared auth middleware + guards: sessionMiddleware, requireAnyAccess, requireGroup(Role), bearerAuthMiddleware, clientIp
│ ├── invites.ts # Invite CRUD (KV invite:{token}, 7d TTL) + acceptInvite (join group as admin/viewer)
│ ├── session.ts # Session CRUD (KV session:{id}), isAdminUser, cookie helpers │ ├── session.ts # Session CRUD (KV session:{id}), isAdminUser, cookie helpers
│ ├── groups.ts # Group CRUD (config:groups), resolveScope, hasAnyAccess, groupAcceptsOwners │ ├── groups.ts # Group CRUD (config:groups), member roles (normalizeGroupMembers/memberRole), resolveScope + role helpers (roleAt/canEditRoutes/canEditGroup)
│ ├── home-routes.ts # landing page (zh/en) │ ├── home-routes.ts # landing page (zh/en)
│ ├── legal-routes.ts # /terms + /privacy pages (zh/en) │ ├── legal-routes.ts # /terms + /privacy pages (zh/en)
│ └── richheader-routes.ts # GET /api/richheader: Open Graph page for Telegram avatar link-preview card │ └── richheader-routes.ts # GET /api/richheader: Open Graph page for Telegram avatar link-preview card
└── lib/ # shared infra └── lib/ # shared infra
├── i18n.ts # loadTranslations (KV i18n:{lang} overrides), t() with param interpolation ├── i18n.ts # loadTranslations (KV i18n:{lang} overrides), t() with param interpolation
├── send-log.ts # SendRecord, recordSend/getSendLog/getSendLogById (D1 send_logs) ├── send-log.ts # SendRecord, recordSend/getSendLog/getSendLogById (D1 send_logs)
├── audit.ts # recordAudit/getAuditLog/pruneAuditLogs (D1 audit_logs, best-effort writes)
├── log.ts # JSON console logger (info/warn/error/fatal) ├── log.ts # JSON console logger (info/warn/error/fatal)
└── locales/ # en.ts, zh.ts translation dictionaries └── locales/ # en.ts, zh.ts translation dictionaries
src/__tests__/ # bun test unit tests (webhook, formatter, discord, telegram, admin, send-log, token-store) src/__tests__/ # bun test unit tests (webhook, formatter, discord, telegram, admin, groups, invites, audit, send-log, token-store)
``` ```
## Responsibilities ## Responsibilities
@ -92,6 +98,9 @@ src/__tests__/ # bun test unit tests (webhook, formatter, discord, te
- Verify Telegram webhook calls (X-Telegram-Bot-Api-Secret-Token when configured) - Verify Telegram webhook calls (X-Telegram-Bot-Api-Secret-Token when configured)
- Filter events by: event type, repo name, actor, action, branch, keyword (regex supported) - Filter events by: event type, repo name, actor, action, branch, keyword (regex supported)
- Filter routes by group owner restriction (`Group.owners`), group source-platform restriction (`Group.providers`: github/gitea), and skip fallback routes whenever a regular route matched; stop evaluating further routes when a matched route has `stop: true` - Filter routes by group owner restriction (`Group.owners`), group source-platform restriction (`Group.providers`: github/gitea), and skip fallback routes whenever a regular route matched; stop evaluating further routes when a matched route has `stop: true`
- Enforce role-based access on every admin API: super admins bypass, `owner` manages the group (routes/members/invites/settings), `admin` edits routes, `viewer` is read-only; legacy `adminIds` groups resolve to `owner` members
- Issue single-use 7-day group invite links (`invite:{token}`); accepting joins as admin/viewer (never owner); `ALLOW_SELF_SIGNUP=1` creates a deterministic personal group (`u-{userId}`) on first login
- Record every admin operation (login/logout, group/route/member/invite changes) to D1 `audit_logs`; the scheduled trigger prunes entries past `AUDIT_RETENTION_DAYS`
- Mention Discord roles on route trigger: route-level `discordRoleIds` are rendered as `<@&id>` into the Discord message `content` (Telegram targets ignore the field) - Mention Discord roles on route trigger: route-level `discordRoleIds` are rendered as `<@&id>` into the Discord message `content` (Telegram targets ignore the field)
- Format 28 event types as platform-neutral messages (Discord embeds + Telegram HTML) - Format 28 event types as platform-neutral messages (Discord embeds + Telegram HTML)
- Route messages to Discord channels/threads and Telegram chats/topics via REST - Route messages to Discord channels/threads and Telegram chats/topics via REST
@ -143,7 +152,8 @@ Rule: no functional change ships without its documentation; docs and code must n
- **Production**: `wrangler secret put <NAME>` for each secret - **Production**: `wrangler secret put <NAME>` for each secret
- **Routes**: KV key `config:routes` (JSON array, empty until configured) - **Routes**: KV key `config:routes` (JSON array, empty until configured)
- **KV namespace**: Required binding for token/state/config/session storage - **KV namespace**: Required binding for token/state/config/session storage
- **D1 database**: Binding `DB` (database `webhooker`, id `214a0104-3235-47c0-b7bf-ddda95f3c8ac`) for `send_logs` + `discord_links` + `telegram_links` tables - **D1 database**: Binding `DB` (database `webhooker`, id `214a0104-3235-47c0-b7bf-ddda95f3c8ac`) for `send_logs` + `audit_logs` + `discord_links` + `telegram_links` tables
- **Access control**: `ADMIN_USER_IDS` (super admins), `ALLOW_SELF_SIGNUP` (optional personal group on first login), `AUDIT_RETENTION_DAYS` (default 90) — all plain env vars, not secrets
- **Discord**: `DISCORD_PUBLIC_KEY` (Interactions Endpoint signature verification, from Discord Developer Portal) and `DISCORD_APPLICATION_ID` (optional, auto-resolved via `GET /oauth2/applications/@me` when omitted) are required for interactions - **Discord**: `DISCORD_PUBLIC_KEY` (Interactions Endpoint signature verification, from Discord Developer Portal) and `DISCORD_APPLICATION_ID` (optional, auto-resolved via `GET /oauth2/applications/@me` when omitted) are required for interactions
- **Telegram**: `TELEGRAM_TOKEN` (Bot API token from BotFather) required for Telegram routes; `TELEGRAM_WEBHOOK_SECRET` (optional secret token for `POST /telegram/webhook` verification); avatars are sent as a link-preview card via the built-in `GET /api/richheader` (overridable with `TELEGRAM_RICH_HEADER_HOST`) - **Telegram**: `TELEGRAM_TOKEN` (Bot API token from BotFather) required for Telegram routes; `TELEGRAM_WEBHOOK_SECRET` (optional secret token for `POST /telegram/webhook` verification); avatars are sent as a link-preview card via the built-in `GET /api/richheader` (overridable with `TELEGRAM_RICH_HEADER_HOST`)
- **Webhook providers**: `GITEA_WEBHOOK_SECRET` (required to receive Gitea webhooks; Gitea signs `X-Gitea-Signature` with the hex HMAC-SHA256 of the body) - **Webhook providers**: `GITEA_WEBHOOK_SECRET` (required to receive Gitea webhooks; Gitea signs `X-Gitea-Signature` with the hex HMAC-SHA256 of the body)
@ -158,7 +168,7 @@ npx wrangler kv namespace create KV
# Update wrangler.jsonc with KV ID # Update wrangler.jsonc with KV ID
npx wrangler d1 create webhooker npx wrangler d1 create webhooker
# Update wrangler.jsonc d1_databases with the database ID # Update wrangler.jsonc d1_databases with the database ID
npm run db:migrate:prod # wrangler d1 migrations apply webhooker --remote (migrations/0001..0003) npm run db:migrate:prod # wrangler d1 migrations apply webhooker --remote (migrations/0001..0005)
npx wrangler deploy npx wrangler deploy
``` ```

View file

@ -65,6 +65,8 @@ npx wrangler dev # Start local dev server
| `TELEGRAM_RICH_HEADER_HOST` | Optional base URL overriding the built-in `GET /api/richheader` for Telegram avatar cards | | `TELEGRAM_RICH_HEADER_HOST` | Optional base URL overriding the built-in `GET /api/richheader` for Telegram avatar cards |
| `BASE_URL` | Public URL for OAuth callbacks and the Telegram webhook sync | | `BASE_URL` | Public URL for OAuth callbacks and the Telegram webhook sync |
| `ADMIN_USER_IDS` | Comma-separated GitHub user IDs (or logins) allowed to access `/admin` | | `ADMIN_USER_IDS` | Comma-separated GitHub user IDs (or logins) allowed to access `/admin` |
| `ALLOW_SELF_SIGNUP` | `1` to give access-less GitHub users a personal group on first login (default off) |
| `AUDIT_RETENTION_DAYS` | Audit-log retention in days for the scheduled cleanup (default 90) |
| `DOCS_URL` | Optional docs site URL used by the landing page | | `DOCS_URL` | Optional docs site URL used by the landing page |
| `GITHUB_REPO_URL` | Optional GitHub repo URL used by the landing page | | `GITHUB_REPO_URL` | Optional GitHub repo URL used by the landing page |
| `LEGAL_CONTACT` | Optional contact shown on `/terms` and `/privacy` | | `LEGAL_CONTACT` | Optional contact shown on `/terms` and `/privacy` |
@ -110,14 +112,16 @@ Routes belong to **groups** (KV `config:groups`) that scope admin access and can
### Web UI (`/admin`) ### Web UI (`/admin`)
The built-in config console lets you manage routes and groups in the browser (add / edit / delete / toggle / reorder), and inspect send logs — no KV access needed: The built-in config console lets you manage routes and groups in the browser (add / edit / delete / toggle / reorder), inspect send logs, manage group members and invite links, and read the audit log — no KV access needed:
1. Set `ADMIN_USER_IDS` to the GitHub user IDs (or logins) allowed to manage the console, e.g. `ADMIN_USER_IDS=12345,RhenCloud`. 1. Set `ADMIN_USER_IDS` to the GitHub user IDs (or logins) allowed to manage the console, e.g. `ADMIN_USER_IDS=12345,RhenCloud`.
2. Visit `/admin` and sign in with GitHub. Only users in the whitelist get access. 2. Visit `/admin` and sign in with GitHub. Users with no access get `403` — unless `ALLOW_SELF_SIGNUP=1` (they receive a personal group) or they follow a group invite link.
3. Changes are written to KV immediately and picked up by the webhook pipeline. 3. Changes are written to KV immediately and picked up by the webhook pipeline.
Sign out at `/admin/logout`. Sign out at `/admin/logout`.
**Access model.** Every group has `members` with a role: `owner` (manage group, members, invites; edit routes), `admin` (edit routes, view logs), or `viewer` (read-only). Super admins (`ADMIN_USER_IDS`) bypass everything. Legacy `adminIds` are read as owners. Owners generate single-use, 7-day invite links from the group page; group admins and viewers can browse, admins edit, owners administer. All admin operations (logins, group/route/member/invite changes) are recorded in the D1 `audit_logs` table, pruned after `AUDIT_RETENTION_DAYS` (default 90).
See `config.example.yaml` for full syntax examples. See `config.example.yaml` for full syntax examples.
### Filter Types ### Filter Types
@ -157,13 +161,18 @@ Set `exclude: true` to invert any filter.
- `GET /admin` — Config console UI - `GET /admin` — Config console UI
- `GET /admin/login` — Start admin sign-in (GitHub OAuth) - `GET /admin/login` — Start admin sign-in (GitHub OAuth)
- `GET /admin/logout` — Sign out - `GET /admin/logout` — Sign out
- `GET /admin/invite?token=…` — Accept a group invite (browser page)
- `GET /admin/api/routes` — List routes - `GET /admin/api/routes` — List routes
- `PUT /admin/api/routes` — Replace routes - `PUT /admin/api/routes` — Replace routes (owner/admin per group)
- `GET /admin/api/groups` — List groups (scoped) - `GET /admin/api/groups` — List groups + your role in each
- `PUT /admin/api/groups` — Replace groups (super admin only) - `PUT /admin/api/groups` — Replace groups (super: all; owner: own groups only)
- `GET /admin/api/groups/:groupId/routes` — List a group's routes - `GET /admin/api/groups/:groupId/routes` — List a group's routes
- `PUT /admin/api/groups/:groupId/routes` — Replace a group's routes - `PUT /admin/api/groups/:groupId/routes` — Replace a group's routes
- `GET /admin/api/me` — Current session / scope - `POST /admin/api/groups/:groupId/invites` — Create invite link (owner)
- `GET /admin/api/groups/:groupId/invites` — List pending invites (owner)
- `DELETE /admin/api/invites/:token` — Revoke an invite (owner)
- `GET /admin/api/audit` — Audit log (scoped)
- `GET /admin/api/me` — Current session / scope / roles
- `GET /admin/api/logs` — Send logs (scoped) - `GET /admin/api/logs` — Send logs (scoped)
- `GET /admin/api/logs/:id` — Single send-log entry - `GET /admin/api/logs/:id` — Single send-log entry

View file

@ -65,6 +65,8 @@ npx wrangler dev # 启动本地开发服务器
| `TELEGRAM_RICH_HEADER_HOST` | 可选;覆盖内置 `GET /api/richheader` 的 Telegram 头像卡片地址 | | `TELEGRAM_RICH_HEADER_HOST` | 可选;覆盖内置 `GET /api/richheader` 的 Telegram 头像卡片地址 |
| `BASE_URL` | 公网地址(用于 OAuth 回调与 Telegram webhook 同步) | | `BASE_URL` | 公网地址(用于 OAuth 回调与 Telegram webhook 同步) |
| `ADMIN_USER_IDS` | 允许访问 `/admin` 的 GitHub 用户 ID或登录名逗号分隔 | | `ADMIN_USER_IDS` | 允许访问 `/admin` 的 GitHub 用户 ID或登录名逗号分隔 |
| `ALLOW_SELF_SIGNUP` | 设为 `1` 时,无权限的 GitHub 用户首次登录自动获得个人分组(默认关闭) |
| `AUDIT_RETENTION_DAYS` | 定时清理时审计日志的保留天数(默认 90 |
| `DOCS_URL` | 可选;落地页使用的文档站点 URL | | `DOCS_URL` | 可选;落地页使用的文档站点 URL |
| `GITHUB_REPO_URL` | 可选;落地页使用的 GitHub 仓库 URL | | `GITHUB_REPO_URL` | 可选;落地页使用的 GitHub 仓库 URL |
| `LEGAL_CONTACT` | 可选;`/terms``/privacy` 页面展示的联系方式 | | `LEGAL_CONTACT` | 可选;`/terms``/privacy` 页面展示的联系方式 |
@ -110,14 +112,16 @@ npx wrangler dev # 启动本地开发服务器
### Web 控制台(`/admin` ### Web 控制台(`/admin`
内置的配置控制台让你在浏览器中管理路由与分组(新增 / 编辑 / 删除 / 开关 / 排序),并查看发送日志——无需操作 KV 内置的配置控制台让你在浏览器中管理路由与分组(新增 / 编辑 / 删除 / 开关 / 排序)、查看发送日志、管理组成员与邀请链接、阅读审计日志——无需操作 KV
1. 设置 `ADMIN_USER_IDS` 为允许管理控制台的 GitHub 用户 ID或登录名例如 `ADMIN_USER_IDS=12345,RhenCloud` 1. 设置 `ADMIN_USER_IDS` 为允许管理控制台的 GitHub 用户 ID或登录名例如 `ADMIN_USER_IDS=12345,RhenCloud`
2. 访问 `/admin` 并用 GitHub 登录,仅白名单内用户可进入。 2. 访问 `/admin` 并用 GitHub 登录。无任何权限的用户收到 `403`——除非开启 `ALLOW_SELF_SIGNUP=1`(自动获得个人分组)或通过分组邀请链接加入。
3. 修改会立即写入 KVwebhook 管线随即生效。 3. 修改会立即写入 KVwebhook 管线随即生效。
`/admin/logout` 退出登录。 `/admin/logout` 退出登录。
**权限模型。** 每个分组都有带角色的 `members``owner`(管理分组、成员、邀请;可编辑路由)、`admin`(编辑路由、查看日志)、`viewer`(只读)。超级管理员(`ADMIN_USER_IDS`)绕过所有角色限制。旧的 `adminIds` 字段按 owner 读取。owner 可从分组页面生成一次性、7 天有效的邀请链接;所有管理操作(登录、分组/路由/成员/邀请变更)都会写入 D1 `audit_logs` 表,并按 `AUDIT_RETENTION_DAYS`(默认 90 天)自动清理。
完整语法示例见 `config.example.yaml` 完整语法示例见 `config.example.yaml`
### 过滤器类型 ### 过滤器类型
@ -157,13 +161,18 @@ npx wrangler dev # 启动本地开发服务器
- `GET /admin` — 配置控制台页面 - `GET /admin` — 配置控制台页面
- `GET /admin/login` — 开始管理员登录GitHub OAuth - `GET /admin/login` — 开始管理员登录GitHub OAuth
- `GET /admin/logout` — 退出登录 - `GET /admin/logout` — 退出登录
- `GET /admin/invite?token=…` — 接受分组邀请(浏览器页面)
- `GET /admin/api/routes` — 列出路由 - `GET /admin/api/routes` — 列出路由
- `PUT /admin/api/routes` — 替换路由 - `PUT /admin/api/routes` — 替换路由(按分组 owner/admin 权限)
- `GET /admin/api/groups` — 列出分组(按权限过滤) - `GET /admin/api/groups` — 列出分组 + 你的角色
- `PUT /admin/api/groups` — 替换分组(仅超级管理员 - `PUT /admin/api/groups` — 替换分组(超管全量owner 仅自己的组
- `GET /admin/api/groups/:groupId/routes` — 列出某分组的路由 - `GET /admin/api/groups/:groupId/routes` — 列出某分组的路由
- `PUT /admin/api/groups/:groupId/routes` — 替换某分组的路由 - `PUT /admin/api/groups/:groupId/routes` — 替换某分组的路由
- `GET /admin/api/me` — 当前会话 / 权限范围 - `POST /admin/api/groups/:groupId/invites` — 创建邀请链接owner
- `GET /admin/api/groups/:groupId/invites` — 列出待接受邀请owner
- `DELETE /admin/api/invites/:token` — 撤销邀请owner
- `GET /admin/api/audit` — 审计日志(按权限过滤)
- `GET /admin/api/me` — 当前会话 / 权限范围 / 角色
- `GET /admin/api/logs` — 发送日志(按权限过滤) - `GET /admin/api/logs` — 发送日志(按权限过滤)
- `GET /admin/api/logs/:id` — 单条发送日志 - `GET /admin/api/logs/:id` — 单条发送日志

View file

@ -6,7 +6,10 @@
groups: groups:
- id: default - id: default
name: "Default" name: "Default"
adminIds: ["your-github-id"] # Members with roles (owner / admin / viewer); legacy adminIds still work and are treated as owners.
members:
- login: "your-github-id"
role: owner
# owners: ["myorg"] # restrict events to this org/user (optional) # owners: ["myorg"] # restrict events to this org/user (optional)
# providers: ["github", "gitea"] # restrict to source platforms (optional; empty = all) # providers: ["github", "gitea"] # restrict to source platforms (optional; empty = all)
# emoji: true # include emoji in messages (default true) # emoji: true # include emoji in messages (default true)

View file

@ -30,6 +30,8 @@ WebHooker requires several secrets to function. For local development, store the
| `TELEGRAM_RICH_HEADER_HOST` | Base URL of an external rich-header service; when unset, the built-in `GET /api/richheader` serves the Telegram avatar card | Built-in `/api/richheader` | | `TELEGRAM_RICH_HEADER_HOST` | Base URL of an external rich-header service; when unset, the built-in `GET /api/richheader` serves the Telegram avatar card | Built-in `/api/richheader` |
| `BASE_URL` | Public URL for OAuth callbacks | `http://localhost:8787` | | `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 | | `ADMIN_USER_IDS` | Comma-separated GitHub user IDs (or logins) allowed to access the Web UI | Disabled |
| `ALLOW_SELF_SIGNUP` | When enabled (`1`/`true`), GitHub users without any group access get a personal group on first login instead of `403` | Disabled |
| `AUDIT_RETENTION_DAYS` | Audit-log retention in days for the scheduled cleanup | `90` |
## Webhook Providers ## Webhook Providers
@ -48,26 +50,31 @@ WebHooker ships with a built-in config console at `/admin` for managing routes i
### Setup ### 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. 1. Configure `ADMIN_USER_IDS` with the GitHub user IDs allowed to manage everything. Logins are also accepted, e.g. `ADMIN_USER_IDS=12345,RhenCloud`. If unset, the console is disabled (unless `ALLOW_SELF_SIGNUP` is enabled).
2. Open `/admin` and sign in with GitHub. 2. Open `/admin` and sign in with GitHub.
3. Only users in the whitelist receive a session cookie; everyone else gets `403`. 3. Users without any access get `403`, except when `ALLOW_SELF_SIGNUP=1` (they receive a personal group) or when they follow a group [invite link](#invites).
### 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/me` | Current session, scope, and groups | | `GET /admin/invite?token=…` | Accept a group invite (browser page) |
| `GET /admin/api/routes` | List routes (admin only) | | `GET /admin/api/me` | Current session, scope, groups, and roles |
| `PUT /admin/api/routes` | Replace routes (admin only) | | `GET /admin/api/routes` | List routes (scoped to access) |
| `GET /admin/api/groups` | List groups (scoped to access) | | `PUT /admin/api/routes` | Replace routes (owner/admin per group) |
| `PUT /admin/api/groups` | Replace groups (super admin only) | | `GET /admin/api/groups` | List groups + the signed-in user's role each |
| `GET /admin/api/groups/:id/routes` | List a group's routes | | `PUT /admin/api/groups` | Replace groups (super: all; owner: own only) |
| `PUT /admin/api/groups/:id/routes` | Replace a group's routes | | `GET /admin/api/groups/:id/routes` | List a group's routes |
| `GET /admin/api/logs` | Send logs (scoped to accessible routes) | | `PUT /admin/api/groups/:id/routes` | Replace a group's routes (owner/admin) |
| `GET /admin/api/logs/:id` | Single send-log entry (scoped) | | `GET /admin/api/logs` | Send logs (scoped to accessible routes) |
| `GET /admin/api/logs/:id` | Single send-log entry (scoped) |
| `POST /admin/api/groups/:id/invites` | Create an invite link (owner) |
| `GET /admin/api/groups/:id/invites` | List pending invites (owner) |
| `DELETE /admin/api/invites/:token` | Revoke an invite (owner) |
| `GET /admin/api/audit` | Audit log (scoped to accessible groups) |
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.
@ -167,29 +174,53 @@ Routes belong to groups. Groups scope admin access and can restrict which events
{ {
"id": "backend-team", "id": "backend-team",
"name": "Backend Team", "name": "Backend Team",
"adminIds": ["rhencloud"], "members": [
{ "login": "rhencloud", "role": "owner" },
{ "login": "octobot", "role": "admin" },
{ "login": "reader", "role": "viewer" }
],
"owners": ["myorg"], "owners": ["myorg"],
"providers": ["github", "gitea"] "providers": ["github", "gitea"]
} }
``` ```
| Field | Type | Required | Description | | Field | Type | Required | Description |
| ----------- | -------- | -------- | ------------------------------------------------------------------------- | | ----------- | -------- | -------- | ---------------------------------------------------------------------------- |
| `id` | string | Yes | Lowercase id (`a-z0-9`, `-`); referenced by each route's `groupId` | | `id` | string | Yes | Lowercase id (`a-z0-9`, `-`); referenced by each route's `groupId` |
| `name` | string | Yes | Human-readable group name | | `name` | string | Yes | Human-readable group name |
| `adminIds` | string[] | Yes | GitHub user IDs or logins who may manage this group's routes | | `members` | object[] | No | `{ login, role }` entries; role is `owner`, `admin`, or `viewer` |
| `owners` | string[] | No | Org/user logins whose events are accepted into this group; empty = all | | `adminIds` | string[] | No | Deprecated legacy field; treated as `members` with role `owner` when present |
| `providers` | string[] | No | Source platforms allowed into this group (`github`, `gitea`); empty = all | | `owners` | string[] | No | Org/user logins whose events are accepted into this group; empty = all |
| `emoji` | boolean | No | Whether to include emoji in this group's messages (default `true`) | | `providers` | string[] | No | Source platforms allowed into this group (`github`, `gitea`); empty = all |
| `emoji` | boolean | No | Whether to include emoji in this group's messages (default `true`) |
### Roles
Every group member has one of three roles. Super admins (`ADMIN_USER_IDS`) always bypass them.
| Role | View routes/logs | Edit routes | Manage members & invites | Edit group settings |
| -------- | ---------------- | ----------- | ------------------------ | ------------------- |
| `owner` | ✓ | ✓ | ✓ | ✓ (except `owners`) |
| `admin` | ✓ | ✓ | ✗ | ✗ |
| `viewer` | ✓ (read-only) | ✗ | ✗ | ✗ |
### Access Model ### Access Model
- **Super admins** (`ADMIN_USER_IDS`) see and edit every group and all routes. - **Super admins** (`ADMIN_USER_IDS`) see and edit every group and all routes; only they can edit a group's `owners` list.
- **Group admins** (`adminIds`) only see and edit the groups they manage; submitting a route outside their groups returns `403`. - **Owners** manage their group's routes, members, invites, name, `emoji`, and `providers`. They cannot remove the last owner or demote themselves when no other owner remains.
- **Admins** edit routes inside their groups and view logs; **viewers** get a read-only console.
- Group admin endpoints operate on a single group at a time via `/admin/api/groups/:id/routes`; `groupId` is forced from the path parameter. - 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. - The `owners` list restricts which event actors (sender logins) the group's routes will dispatch at all.
- The `providers` list restricts which forge's events (`github`, `gitea`) the group's routes will dispatch. This lets you keep GitHub and Gitea groups separate even when org/user names collide. - The `providers` list restricts which forge's events (`github`, `gitea`) the group's routes will dispatch. This lets you keep GitHub and Gitea groups separate even when org/user names collide.
### Invites
Owners (and super admins) can create single-use invite links valid for 7 days from the group's _Members_ panel. Accepting an invite adds the user with the invited role (`admin` or `viewer` — never `owner`); an existing `viewer` is upgraded to `admin`. Invites are stored in KV as `invite:{token}`.
### Self Sign-up
With `ALLOW_SELF_SIGNUP=1`, a GitHub user who has no group access gets a personal group (`u-{userId}`, owned by them) on first login instead of a `403`. This is the entry point for a fully self-service SaaS install; disable it to keep the console invite-only.
## Filter Types ## Filter Types
See the [Filter Tutorial](./filters) for a hands-on guide with worked examples. See the [Filter Tutorial](./filters) for a hands-on guide with worked examples.
@ -230,6 +261,7 @@ Filters accept either a single string or an array of strings:
| `token:{userId}` | `{ userId, accessToken, expiresAt, refreshToken? }` | 0.9 × token expiry | | `token:{userId}` | `{ userId, accessToken, expiresAt, refreshToken? }` | 0.9 × token expiry |
| `token-reverse:{sha256}` | User id for reverse lookup by token | 0.9 × token expiry | | `token-reverse:{sha256}` | User id for reverse lookup by token | 0.9 × token expiry |
| `state:{hex}` | `{ redirectTo, expiresAt, discordUserId?, telegramUserId?, telegramChatId? }` | 600 seconds | | `state:{hex}` | `{ redirectTo, expiresAt, discordUserId?, telegramUserId?, telegramChatId? }` | 600 seconds |
| `invite:{token}` | `{ groupId, role, expiresAt, createdBy, note? }` | 7 days |
| `delivery:{id}` | Webhook delivery id (dedup marker) | 300 seconds | | `delivery:{id}` | Webhook delivery id (dedup marker) | 300 seconds |
| `msg:{routeId}:{key}:{target}` | Message id tracking for in-place updates (e.g. `workflow_run`) | 7 days | | `msg:{routeId}:{key}:{target}` | Message id tracking for in-place updates (e.g. `workflow_run`) | 7 days |
| `cmd:guild:{id}` | Guild id whose commands were registered (dedup) | Permanent | | `cmd:guild:{id}` | Guild id whose commands were registered (dedup) | Permanent |
@ -239,10 +271,13 @@ Filters accept either a single string or an array of strings:
## D1 Storage Layout ## D1 Storage Layout
The D1 database (`DB` binding, database `webhooker`) holds three tables: The D1 database (`DB` binding, database `webhooker`) holds four tables:
| Table | Purpose | | Table | Purpose |
| ---------------- | ---------------------------------------------------------------------------------------------- | | ---------------- | ---------------------------------------------------------------------------------------------- |
| `send_logs` | One row per dispatch attempt (route id, event, target, ok/error, duration, error code, detail) | | `send_logs` | One row per dispatch attempt (route id, event, target, ok/error, duration, error code, detail) |
| `audit_logs` | One row per admin operation (login/logout, group/route/member/invite changes) |
| `discord_links` | Maps `discord_user_id``github_user_id` for `/gh` Discord commands | | `discord_links` | Maps `discord_user_id``github_user_id` for `/gh` Discord commands |
| `telegram_links` | Maps `telegram_user_id``github_user_id` for `/gh` Telegram commands | | `telegram_links` | Maps `telegram_user_id``github_user_id` for `/gh` Telegram commands |
`audit_logs` is pruned automatically by the scheduled trigger after `AUDIT_RETENTION_DAYS` (default 90).

View file

@ -81,6 +81,8 @@ If the database already has these tables/columns (e.g. previously migrated with
npx wrangler d1 execute webhooker --remote --file ./migrations/0001_init.sql npx wrangler d1 execute webhooker --remote --file ./migrations/0001_init.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0002_log_detail.sql npx wrangler d1 execute webhooker --remote --file ./migrations/0002_log_detail.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0003_telegram_links.sql npx wrangler d1 execute webhooker --remote --file ./migrations/0003_telegram_links.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0004_add_group_id.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0005_audit_logs.sql
``` ```
::: :::

View file

@ -26,6 +26,8 @@ WebHooker 需要多个密钥才能运行。本地开发时存储在 `.dev.vars`
| --------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------- | | --------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------- |
| `BASE_URL` | OAuth 回调的公开 URL | `http://localhost:8787` | | `BASE_URL` | OAuth 回调的公开 URL | `http://localhost:8787` |
| `ADMIN_USER_IDS` | 允许访问 WebUI 的 GitHub 用户 ID或登录名逗号分隔 | 未设置时 WebUI 关闭 | | `ADMIN_USER_IDS` | 允许访问 WebUI 的 GitHub 用户 ID或登录名逗号分隔 | 未设置时 WebUI 关闭 |
| `ALLOW_SELF_SIGNUP` | 开启(`1`/`true`)后,没有任何分组权限的 GitHub 用户首次登录会自动获得个人分组而非 403 | 关闭 |
| `AUDIT_RETENTION_DAYS` | 定时清理时审计日志的保留天数 | `90` |
| `DISCORD_PUBLIC_KEY` | Discord 应用的公钥(开发者门户获取),交互功能必需 | 未设置时交互返回 401 | | `DISCORD_PUBLIC_KEY` | Discord 应用的公钥(开发者门户获取),交互功能必需 | 未设置时交互返回 401 |
| `DISCORD_APPLICATION_ID` | Discord 应用 ID省略时自动获取 | 自动获取 | | `DISCORD_APPLICATION_ID` | Discord 应用 ID省略时自动获取 | 自动获取 |
| `TELEGRAM_WEBHOOK_SECRET` | `POST /telegram/webhook` 验签密钥X-Telegram-Bot-Api-Secret-Token | 未设置时不校验 | | `TELEGRAM_WEBHOOK_SECRET` | `POST /telegram/webhook` 验签密钥X-Telegram-Bot-Api-Secret-Token | 未设置时不校验 |
@ -48,26 +50,31 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
### 设置 ### 设置
1. 配置 `ADMIN_USER_IDS`,填写允许管理路由的 GitHub 用户 ID也支持登录名例如 `ADMIN_USER_IDS=12345,RhenCloud`。未设置时控制台禁用。 1. 配置 `ADMIN_USER_IDS`,填写允许管理路由的 GitHub 用户 ID也支持登录名例如 `ADMIN_USER_IDS=12345,RhenCloud`。未设置时控制台禁用(除非开启 `ALLOW_SELF_SIGNUP`
2. 打开 `/admin` 并使用 GitHub 登录。 2. 打开 `/admin` 并使用 GitHub 登录。
3. 只有白名单中的用户会获得会话 Cookie其他人收到 `403` 3. 没有任何权限的用户收到 `403`,除非开启 `ALLOW_SELF_SIGNUP=1`(自动获得个人分组)或通过分组[邀请链接](#邀请)加入
### 端点 ### 端点
| 端点 | 说明 | | 端点 | 说明 |
| ---------------------------------- | ---------------------------- | | ------------------------------------- | ------------------------------------ |
| `GET /admin` | 配置控制台页面 | | `GET /admin` | 配置控制台页面 |
| `GET /admin/login` | 开始 GitHub OAuth 登录 | | `GET /admin/login` | 开始 GitHub OAuth 登录 |
| `GET /admin/logout` | 销毁会话 | | `GET /admin/logout` | 销毁会话 |
| `GET /admin/api/me` | 当前会话、权限范围和分组 | | `GET /admin/invite?token=…` | 接受分组邀请(浏览器页面) |
| `GET /admin/api/routes` | 列出路由(仅管理员) | | `GET /admin/api/me` | 当前会话、权限范围、分组和角色 |
| `PUT /admin/api/routes` | 替换路由(仅管理员) | | `GET /admin/api/routes` | 列出路由(按权限过滤) |
| `GET /admin/api/groups` | 列出分组(按权限过滤) | | `PUT /admin/api/routes` | 替换路由(按分组 owner/admin 权限) |
| `PUT /admin/api/groups` | 替换分组(仅超级管理员) | | `GET /admin/api/groups` | 列出分组 + 当前用户在各组的角色 |
| `GET /admin/api/groups/:id/routes` | 列出某分组的路由 | | `PUT /admin/api/groups` | 替换分组超管全量owner 仅自己的组) |
| `PUT /admin/api/groups/:id/routes` | 替换某分组的路由 | | `GET /admin/api/groups/:id/routes` | 列出某分组的路由 |
| `GET /admin/api/logs` | 发送日志(按可访问路由过滤) | | `PUT /admin/api/groups/:id/routes` | 替换某分组的路由owner/admin |
| `GET /admin/api/logs/:id` | 单条发送日志(按权限过滤) | | `GET /admin/api/logs` | 发送日志(按可访问路由过滤) |
| `GET /admin/api/logs/:id` | 单条发送日志(按权限过滤) |
| `POST /admin/api/groups/:id/invites` | 创建邀请链接owner |
| `GET /admin/api/groups/:id/invites` | 列出待接受邀请owner |
| `DELETE /admin/api/invites/:token` | 撤销邀请owner |
| `GET /admin/api/audit` | 审计日志(按可访问分组过滤) |
控制台支持新增、编辑、删除和开关路由。保存后立即写入 KV `config:routes` 并使配置缓存失效,下一次 webhook 处理即会生效。 控制台支持新增、编辑、删除和开关路由。保存后立即写入 KV `config:routes` 并使配置缓存失效,下一次 webhook 处理即会生效。
@ -167,29 +174,53 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
{ {
"id": "backend-team", "id": "backend-team",
"name": "后端团队", "name": "后端团队",
"adminIds": ["rhencloud"], "members": [
{ "login": "rhencloud", "role": "owner" },
{ "login": "octobot", "role": "admin" },
{ "login": "reader", "role": "viewer" }
],
"owners": ["myorg"], "owners": ["myorg"],
"providers": ["github", "gitea"] "providers": ["github", "gitea"]
} }
``` ```
| 字段 | 类型 | 必需 | 说明 | | 字段 | 类型 | 必需 | 说明 |
| ----------- | -------- | ---- | ----------------------------------------------------------- | | ----------- | -------- | ---- | ------------------------------------------------------------------ |
| `id` | string | 是 | 小写 id`a-z0-9``-`),由每条路由的 `groupId` 引用 | | `id` | string | 是 | 小写 id`a-z0-9``-`),由每条路由的 `groupId` 引用 |
| `name` | string | 是 | 可读的分组名称 | | `name` | string | 是 | 可读的分组名称 |
| `adminIds` | string[] | 是 | 可管理该分组路由的 GitHub 用户 ID 或登录名 | | `members` | object[] | 否 | `{ login, role }` 列表;角色为 `owner``admin``viewer` |
| `owners` | string[] | 否 | 允许事件进入该分组的组织/用户登录名;为空表示不限制 | | `adminIds` | string[] | 否 | 已废弃的旧字段;存在时按 role 为 `owner` 的成员处理 |
| `providers` | string[] | 否 | 允许进入该分组的来源平台(`github``gitea`);为空表示全部 | | `owners` | string[] | 否 | 允许事件进入该分组的组织/用户登录名;为空表示不限制 |
| `emoji` | boolean | 否 | 是否在该分组消息中显示 emoji默认 `true` | | `providers` | string[] | 否 | 允许进入该分组的来源平台(`github``gitea`);为空表示全部 |
| `emoji` | boolean | 否 | 是否在该分组消息中显示 emoji默认 `true` |
### 角色
每个分组成员拥有三种角色之一。超级管理员(`ADMIN_USER_IDS`)始终绕过角色限制。
| 角色 | 查看路由/日志 | 编辑路由 | 管理成员与邀请 | 编辑分组设置 |
| -------- | ------------- | -------- | -------------- | ------------ |
| `owner` | ✓ | ✓ | ✓ | ✓(`owners` 除外) |
| `admin` | ✓ | ✓ | ✗ | ✗ |
| `viewer` | ✓(只读) | ✗ | ✗ | ✗ |
### 权限模型 ### 权限模型
- **超级管理员**`ADMIN_USER_IDS`)可查看和编辑所有分组及全部路由。 - **超级管理员**`ADMIN_USER_IDS`)可查看和编辑所有分组及全部路由;只有他们能修改分组的 `owners` 列表。
- **分组管理员**`adminIds`)只能查看和编辑其管理的分组;提交其分组之外的路由返回 `403` - **owner** 管理本组的路由、成员、邀请、名称、`emoji``providers`;不能移除最后一位 owner也没有其他 owner 时不能把自己降级。
- **admin** 可编辑本组路由并查看日志;**viewer** 只读控制台。
- 分组管理端点通过 `/admin/api/groups/:id/routes` 一次只操作一个分组;`groupId` 由路径参数强制指定。 - 分组管理端点通过 `/admin/api/groups/:id/routes` 一次只操作一个分组;`groupId` 由路径参数强制指定。
- `owners` 列表限定哪些事件参与者(发送者登录名)的事件会被该分组的路由投递。 - `owners` 列表限定哪些事件参与者(发送者登录名)的事件会被该分组的路由投递。
- `providers` 列表限定哪个 forge`github``gitea`)的事件会被该分组的路由投递。即使组织/用户同名,也可以借此将 GitHub 与 Gitea 分组区分开。 - `providers` 列表限定哪个 forge`github``gitea`)的事件会被该分组的路由投递。即使组织/用户同名,也可以借此将 GitHub 与 Gitea 分组区分开。
### 邀请
owner及超级管理员可在分组的「成员」面板创建一次性邀请链接7 天内有效。接受邀请后用户以邀请角色(`admin``viewer`,绝不授予 `owner`)加入;已有的 `viewer` 会被升级为 `admin`。邀请存储在 KV `invite:{token}`
### 自助注册
开启 `ALLOW_SELF_SIGNUP=1` 后,没有分组权限的 GitHub 用户首次登录会获得一个由自己担任 owner 的个人分组(`u-{userId}`),而不是 `403`。这是全自助 SaaS 部署的入口;关闭它则控制台保持仅邀请制。
## 过滤器类型 ## 过滤器类型
实操指南见[过滤器教程](./filters),包含完整示例。 实操指南见[过滤器教程](./filters),包含完整示例。
@ -230,6 +261,7 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
| `token:{userId}` | `{ userId, accessToken, expiresAt, refreshToken? }` | 0.9 × Token 有效期 | | `token:{userId}` | `{ userId, accessToken, expiresAt, refreshToken? }` | 0.9 × Token 有效期 |
| `token-reverse:{sha256}` | 用于按 Token 反查的用户 id | 0.9 × Token 有效期 | | `token-reverse:{sha256}` | 用于按 Token 反查的用户 id | 0.9 × Token 有效期 |
| `state:{hex}` | `{ redirectTo, expiresAt, discordUserId?, telegramUserId?, telegramChatId? }` | 600 秒 | | `state:{hex}` | `{ redirectTo, expiresAt, discordUserId?, telegramUserId?, telegramChatId? }` | 600 秒 |
| `invite:{token}` | `{ groupId, role, expiresAt, createdBy, note? }` | 7 天 |
| `delivery:{id}` | Webhook 投递 id去重标记 | 300 秒 | | `delivery:{id}` | Webhook 投递 id去重标记 | 300 秒 |
| `msg:{routeId}:{key}:{target}` | 原地更新用消息 id 追踪(如 `workflow_run` | 7 天 | | `msg:{routeId}:{key}:{target}` | 原地更新用消息 id 追踪(如 `workflow_run` | 7 天 |
| `cmd:guild:{id}` | 已注册命令的服务器 id去重标记 | 永久 | | `cmd:guild:{id}` | 已注册命令的服务器 id去重标记 | 永久 |
@ -239,10 +271,13 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
## D1 存储布局 ## D1 存储布局
D1 数据库(`DB` 绑定,数据库 `webhooker`)包含张表: D1 数据库(`DB` 绑定,数据库 `webhooker`)包含张表:
| 表 | 用途 | | 表 | 用途 |
| ---------------- | ---------------------------------------------------------------------- | | ---------------- | ---------------------------------------------------------------------- |
| `send_logs` | 每次分发尝试一行(路由 id、事件、目标、成功/失败、耗时、错误码、详情) | | `send_logs` | 每次分发尝试一行(路由 id、事件、目标、成功/失败、耗时、错误码、详情) |
| `audit_logs` | 每次管理操作一行(登录/登出、分组/路由/成员/邀请变更) |
| `discord_links` | 映射 `discord_user_id``github_user_id`,用于 Discord `/gh` 命令 | | `discord_links` | 映射 `discord_user_id``github_user_id`,用于 Discord `/gh` 命令 |
| `telegram_links` | 映射 `telegram_user_id``github_user_id`,用于 Telegram `/gh` 命令 | | `telegram_links` | 映射 `telegram_user_id``github_user_id`,用于 Telegram `/gh` 命令 |
`audit_logs` 由定时触发器按 `AUDIT_RETENTION_DAYS`(默认 90自动清理。

View file

@ -80,6 +80,8 @@ npm run db:migrate # 将迁移应用到本地Miniflare数据库
npx wrangler d1 execute webhooker --remote --file ./migrations/0001_init.sql npx wrangler d1 execute webhooker --remote --file ./migrations/0001_init.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0002_log_detail.sql npx wrangler d1 execute webhooker --remote --file ./migrations/0002_log_detail.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0003_telegram_links.sql npx wrangler d1 execute webhooker --remote --file ./migrations/0003_telegram_links.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0004_add_group_id.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0005_audit_logs.sql
``` ```
::: :::