MF-3: Per-forge webhook ingress + signature verification #294

Closed
opened 2026-04-23 23:33:12 +00:00 by code-lead · 0 comments
Collaborator

As a platform engineer, I want webhook deliveries from Forgejo, GitHub, and GitLab routed into the same domain dispatch pipeline (normalised at the HTTP boundary) so that the rest of the service stays forge-agnostic and signature verification is uniformly enforced.

Acceptance criteria

Routes

  • Three HTTP routes: /webhooks/forgejo, /webhooks/github, /webhooks/gitlab.
  • Legacy /webhooks stays as a Forgejo alias — zero-cost, indefinite.
  • Unknown forge events log + return 204 (no-op), never 500.

Signature verification (per forge)

  • Forgejo: HMAC-SHA256 of body with webhook_secret, header X-Forgejo-Signature (current behaviour preserved).
  • GitHub: HMAC-SHA256 with X-Hub-Signature-256: sha256=<hex> (prefix stripped before compare).
  • GitLab: static token equality check on X-Gitlab-Token header. Documented in code + docs as plaintext (not HMAC); per-forge secrets MUST be unique per repo to limit blast radius.
  • Failure → 403.

Normaliser

  • New apps/server/src/http/webhook-normalize.ts with pure functions:
    • normalizeGitHubPayload(event, body) → ForgeEvent
    • normalizeGitLabPayload(event, body) → ForgeEvent
    • normalizeForgejoPayload(event, body) → ForgeEvent (either refactor the existing dispatch or call through the shared shape).
  • Pure — no I/O, no logging side effects beyond a returned diagnostic shape.

Shared event union

  • ForgeEvent discriminated union exported from packages/shared covering every event the Forgejo dispatch currently handles: issues.assigned, issues.labeled, issues.closed, pull_request.opened, pull_request.closed, pull_request_review.request, pull_request_review.approved, pull_request_review.changes_requested, issue_comment.created, action_run.* / workflow_run.* (CI status).
  • Events the service doesn't currently dispatch round-trip through ForgeEvent.Unhandled — still 204, never lost.

Tests

  • webhook-normalize.test.ts: each forge's real-world payload (captured fixture, trimmed of PII) normalises to the same ForgeEvent for equivalent actions.
  • Signature-fail scenarios per forge return 403.
  • Unknown event type returns 204.

Out of scope

  • Rewriting Forgejo dispatch from switch (event) to the normalised ForgeEvent union in one go. Can be a follow-up once the union is stable; keep today's Forgejo handlers as-is, just call the normaliser for parity.
  • Replaying missed webhooks after outage — separate reliability concern.

References

  • Spec: specs/multi-forge.md § MF-3.
  • apps/server/src/http/webhook.ts — current Forgejo-only entry.
  • apps/server/src/http/webhook-handlers.ts — event handlers that receive the normalised events.
  • Forgejo webhook signature doc (existing repo usage).

Dependencies

  • Blocks on MF-4 (adapter factory) — the handler dispatch reads per-repo forge binding to know which normaliser fits the incoming path.
  • Independent of adapter internals — operates on HTTP bodies only. Can start as soon as MF-4 lands in parallel with MF-1.
  • Depends on #295 (MF-4)
As a platform engineer, I want webhook deliveries from Forgejo, GitHub, and GitLab routed into the same domain dispatch pipeline (normalised at the HTTP boundary) so that the rest of the service stays forge-agnostic and signature verification is uniformly enforced. ## Acceptance criteria ### Routes - [ ] Three HTTP routes: `/webhooks/forgejo`, `/webhooks/github`, `/webhooks/gitlab`. - [ ] Legacy `/webhooks` stays as a Forgejo alias — zero-cost, indefinite. - [ ] Unknown forge events log + return `204` (no-op), never `500`. ### Signature verification (per forge) - [ ] **Forgejo**: HMAC-SHA256 of body with `webhook_secret`, header `X-Forgejo-Signature` (current behaviour preserved). - [ ] **GitHub**: HMAC-SHA256 with `X-Hub-Signature-256: sha256=<hex>` (prefix stripped before compare). - [ ] **GitLab**: static token equality check on `X-Gitlab-Token` header. Documented in code + docs as plaintext (not HMAC); per-forge secrets MUST be unique per repo to limit blast radius. - [ ] Failure → `403`. ### Normaliser - [ ] New `apps/server/src/http/webhook-normalize.ts` with pure functions: - `normalizeGitHubPayload(event, body) → ForgeEvent` - `normalizeGitLabPayload(event, body) → ForgeEvent` - `normalizeForgejoPayload(event, body) → ForgeEvent` (either refactor the existing dispatch or call through the shared shape). - [ ] Pure — no I/O, no logging side effects beyond a returned diagnostic shape. ### Shared event union - [ ] `ForgeEvent` discriminated union exported from `packages/shared` covering every event the Forgejo dispatch currently handles: `issues.assigned`, `issues.labeled`, `issues.closed`, `pull_request.opened`, `pull_request.closed`, `pull_request_review.request`, `pull_request_review.approved`, `pull_request_review.changes_requested`, `issue_comment.created`, `action_run.*` / `workflow_run.*` (CI status). - [ ] Events the service doesn't currently dispatch round-trip through `ForgeEvent.Unhandled` — still `204`, never lost. ### Tests - [ ] `webhook-normalize.test.ts`: each forge's real-world payload (captured fixture, trimmed of PII) normalises to the same `ForgeEvent` for equivalent actions. - [ ] Signature-fail scenarios per forge return `403`. - [ ] Unknown event type returns `204`. ## Out of scope - Rewriting Forgejo dispatch from `switch (event)` to the normalised `ForgeEvent` union in one go. Can be a follow-up once the union is stable; keep today's Forgejo handlers as-is, just call the normaliser for parity. - Replaying missed webhooks after outage — separate reliability concern. ## References - Spec: [`specs/multi-forge.md`](../src/branch/main/specs/multi-forge.md) § MF-3. - `apps/server/src/http/webhook.ts` — current Forgejo-only entry. - `apps/server/src/http/webhook-handlers.ts` — event handlers that receive the normalised events. - Forgejo webhook signature doc (existing repo usage). ## Dependencies - **Blocks on MF-4** (adapter factory) — the handler dispatch reads per-repo forge binding to know which normaliser fits the incoming path. - Independent of adapter internals — operates on HTTP bodies only. Can start as soon as MF-4 lands in parallel with MF-1. <!-- machine-parseable deps for the deps.ts body fallback; native POST /dependencies is currently returning HTTP 404 on this Forgejo. --> - Depends on #295 (MF-4)
code-lead removed their assignment 2026-04-23 23:35:06 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks#294
No description provided.