feat: per-group webhook ingress, custom webhooks, GitHub App tenant isolation

Add POST /webhook/{groupId} with per-group secrets (KV tenant:{groupId}), a custom provider (X-WebHooker-Signature HMAC, arbitrary JSON -> custom events through the route pipeline), and GitHub App installation isolation (Group.installationId) with automatic provisioning on installation.created (inst-{id} groups or binding matching owners groups). Includes WebhookPanel admin UI, custom route template, docs and 157 passing tests.
This commit is contained in:
RhenCloud 2026-08-13 09:24:50 +08:00
parent 0b078d938b
commit b600f02027
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
34 changed files with 1711 additions and 183 deletions

View file

@ -50,6 +50,25 @@ const en: Dict = {
"members.errLastOwner": "Cannot remove the last owner.",
"members.inviteOk": "Invite accepted — welcome!",
"members.inviteBad": "This invite is invalid or expired.",
"webhook.title": "Webhook endpoint",
"webhook.note": "(per-group URL + secret)",
"webhook.empty": "Not configured. Generate a secret to enable the group's own webhook endpoint.",
"webhook.url": "URL",
"webhook.secret": "Secret",
"webhook.noSecret": "— no secret configured —",
"webhook.secretHidden":
"The secret is only shown once after generating/regenerating. Regenerate to reveal a new one.",
"webhook.copy": "Copy",
"webhook.copied": "Copied!",
"webhook.generate": "Generate secret",
"webhook.regenerate": "Regenerate",
"webhook.disable": "Disable",
"webhook.usageTitle": "How to use",
"webhook.usageGitHub":
"GitHub: repo/org Settings → Webhooks → Add webhook → payload URL = the URL above, Content type = application/json, Secret = the secret above.",
"webhook.usageGitea": "Gitea: repo Settings → Webhooks → Add Webhook → same URL + secret.",
"webhook.usageCustom":
"Custom: POST any JSON signed with X-WebHooker-Signature (sha256 HMAC). Create a route with event = custom to receive it:",
"groups.empty": "No groups yet. Groups scope routes by org/user and delegate access.",
"groups.createFirst": "Create your first group",
"groups.admins": "ADMINS",
@ -95,6 +114,7 @@ const en: Dict = {
"templates.createDelete": "Create / Delete",
"templates.member": "Member",
"templates.commitComment": "Commit Comment",
"templates.customWebhook": "Custom Webhook",
"routeEditor.name": "Name",
"routeEditor.namePlaceholder": "My Route",
"routeEditor.id": "ID",
@ -167,6 +187,12 @@ const en: Dict = {
"groupEditor.providersNote": "(leave both unchecked = all)",
"groupEditor.providersHint":
"Only webhook events from the checked forges (GitHub / Gitea) enter this group's routes.",
"groupEditor.installationId": "GitHub App installation ID",
"groupEditor.installationIdNote": "(optional)",
"groupEditor.installationIdPlaceholder": "e.g. 12345678",
"groupEditor.installationIdHint":
"Bind this group to one GitHub App installation (the org/user that installed the app). Only events from that installation enter this group's routes; leave empty to allow any installation (still filtered by Owner scope).",
"groupEditor.errInstallationId": "Installation ID must be a positive integer",
"groupEditor.emoji": "Show emojis in messages",
"groupEditor.logTarget": "Webhook log channel",
"groupEditor.logTargetNote": "(optional)",
@ -276,6 +302,24 @@ const zh: Dict = {
"members.errLastOwner": "不能移除最后一位 owner。",
"members.inviteOk": "邀请已接受 —— 欢迎!",
"members.inviteBad": "该邀请无效或已过期。",
"webhook.title": "Webhook 入口",
"webhook.note": "(分组独立的 URL + secret",
"webhook.empty": "未配置。生成 secret 即可启用该分组的独立 webhook 入口。",
"webhook.url": "URL",
"webhook.secret": "Secret",
"webhook.noSecret": "—— 未配置 secret ——",
"webhook.secretHidden": "secret 仅在生成/重新生成时显示一次。如需查看请重新生成。",
"webhook.copy": "复制",
"webhook.copied": "已复制!",
"webhook.generate": "生成 secret",
"webhook.regenerate": "重新生成",
"webhook.disable": "停用",
"webhook.usageTitle": "使用方法",
"webhook.usageGitHub":
"GitHub仓库/组织 Settings → Webhooks → Add webhook → Payload URL 填上面的 URLContent type 选 application/jsonSecret 填上面的 secret。",
"webhook.usageGitea": "Gitea仓库 Settings → Webhooks → Add Webhook填写同样的 URL 和 secret。",
"webhook.usageCustom":
"自定义:用 X-WebHooker-Signaturesha256 HMAC签名任意 JSON 后 POST。创建一条 event = custom 的路由即可接收:",
"groups.admins": "管理员",
"groups.owners": "来源",
"groups.members": "成员",
@ -319,6 +363,7 @@ const zh: Dict = {
"templates.createDelete": "创建 / 删除",
"templates.member": "协作者",
"templates.commitComment": "提交评论",
"templates.customWebhook": "自定义 Webhook",
"routeEditor.name": "名称",
"routeEditor.namePlaceholder": "我的路由",
"routeEditor.id": "ID",
@ -390,6 +435,12 @@ const zh: Dict = {
"groupEditor.providersNote": "(都不勾选 = 全部来源)",
"groupEditor.providersHint":
"只有来自所勾选 forgeGitHub / Gitea的 webhook 事件才会进入本分组的路由。",
"groupEditor.installationId": "GitHub App 安装 ID",
"groupEditor.installationIdNote": "(可选)",
"groupEditor.installationIdPlaceholder": "如 12345678",
"groupEditor.installationIdHint":
"将本分组绑定到一个 GitHub App 安装(安装了 App 的组织/用户)。只有来自该安装的事件才会进入本分组的路由;留空表示接受任意安装(仍受来源限定过滤)。",
"groupEditor.errInstallationId": "安装 ID 必须为正整数",
"groupEditor.emoji": "消息中显示表情符号",
"groupEditor.logTarget": "Webhook 日志频道",
"groupEditor.logTargetNote": "(可选)",