WebHooker/docs/guide/groups.md
RhenCloud 25ebae4ae5
feat(storage): migrate config, dedup and delivery state to D1
- Move oversized queue payloads from KV to R2 (PAYLOAD binding, webhooks/YYYY/MM/DD/*.json, KV queue:payload:* fallback)
- Persist routes/groups to D1 (d1_routes/d1_groups) with memory -> KV -> D1 three-tier cache, seeded from legacy KV config keys
- Move webhook dedup (dedup_keys), delivery state (delivery_state) and message tracking (message_tracking) to D1 via canUseD1 probe with automatic KV fallback
- Batch send_logs inserts (recordSendBatch) and add group_id/ts index
- Add storage-prune scheduled task for expired dedup/state/tracking rows
- Add TTL to invite:group:{id} index and audit all ephemeral KV keys
- Add D1 indexes for the new tables
- Sync AGENTS.md, README.md/zh and docs/ (en/zh) with the new storage layout
2026-08-17 15:01:20 +08:00

7.8 KiB
Raw Blame History

Groups & Access Control

Routes belong to groups. Groups scope admin access and can restrict which events flow into them. They are stored in D1 (d1_groups, seeded from the legacy KV config:groups key on first load), managed via the Web UI or the Admin API. At most 100 groups can be saved per instance.

Group Schema

{
  "id": "backend-team",
  "name": "Backend Team",
  "members": [
    { "login": "rhencloud", "role": "owner" },
    { "login": "octobot", "role": "admin" },
    { "login": "reader", "role": "viewer" }
  ],
  "owners": ["myorg"],
  "providers": ["github", "gitea"],
  "installationId": 12345678,
  "logTarget": { "platform": "discord", "channelId": "123456789", "threadId": "987654321" }
}
Field Type Required Description
id string Yes Lowercase id (a-z0-9, -); referenced by each route's groupId. Editable: renaming a group re-points its routes, per-group webhook secret and pending invites
name string Yes Human-readable group name
members object[] No { login, role } entries; role is owner, admin, or viewer
adminIds string[] No Deprecated legacy field; treated as members with role owner when present
owners string[] No Org/user logins whose events are accepted into this group; empty = all
providers string[] No Source platforms allowed into this group (github, gitea); empty = all
installationId number No GitHub App installation id bound to this group; only that installation's events are accepted (empty = all)
emoji boolean No Whether to include emoji in this group's messages (default true)
forgeSources object[] No Forge hosts: { host, type, name? } entries (type is github or gitea, name is an optional display label) that label this group's message footers; empty = no label
lang string No Message language for every route in this group (e.g. en, zh; custom via KV i18n:<lang>) — see Message Language — defaults to en
logTarget object No Webhook log channel: a Discord { platform, channelId, threadId? } or Telegram { platform, chatId, topicId? } target that receives a summary of every webhook the group's routes dispatch

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

  • Super admins (ADMIN_USER_IDS) see and edit every group and all routes; only they can edit a group's owners list.
  • Owners manage their group's routes, members, invites, name, id, 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.
  • 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.

Webhook Log Channel

A group may set logTarget to a Discord channel/thread or Telegram chat/topic. Whenever the group's routes dispatch a webhook, a single summary message is sent there: the event type/action, the repo, the delivery id, and one line per route×target with an / outcome (including the error for failed sends; at most the first 10 lines are listed, the rest is summarized as +N). The message is green when every dispatch succeeded and red when any failed. The summary uses the group's message language. Log messages are sent best-effort and are not themselves recorded in the D1 send log.

Forge Source Label

A group defines the forges it receives events from via forgeSources, a list of { host, type, name? } entries (type is github or gitea). Every message this group's routes send carries the footer label of the first entry whose type matches the event's provider and whose host matches the repository URL's hostname (GitHub events match github.com). The label is the entry's optional name — falling back to the host — so two self-hosted Gitea instances can be shown as "内网 Gitea" / "Git2 仓库" while matched by their distinct hosts:

{
  "forgeSources": [
    { "host": "github.com", "type": "github", "name": "GitHub 主站" },
    { "host": "git1.example.com", "type": "gitea", "name": "内网 Gitea" },
    { "host": "git2.example.com", "type": "gitea" }
  ]
}
  • Discord — the embed footer shows the label next to the repo (内网 Gitea · acme/widget) with a site icon as the footer icon: GitHub's fluidicon.png or the Gitea instance's /assets/img/favicon.png (both raster PNGs — Discord silently ignores .ico favicons).
  • Telegram — the footer line starts with the hyperlinked label ([内网 Gitea](https://git1.example.com)).
  • Events whose repository host has no matching entry (e.g. custom webhooks, or a repo hosted elsewhere) get no label.

The label is independent of Group.emoji and follows every message that group dispatches (including in-place edits of workflow/check messages).

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.