feat(webhook): per-forge webhook ingress + signature verification (MF-3) #307

Closed
dev wants to merge 2 commits from dev/294 into main
Collaborator

Three new routes /webhooks/forgejo, /webhooks/github, /webhooks/gitlab with forge-specific signature verification (HMAC-SHA256 for Forgejo/GitHub, static token equality for GitLab). Legacy /webhooks alias kept indefinitely. Unknown events return 204; signature failures return 403.

New ForgeEvent discriminated union in @claude-hooks/shared covers all events the Forgejo dispatch currently handles. Pure normaliser functions (normalizeForgejoPayload, normalizeGitHubPayload, normalizeGitLabPayload) in webhook-normalize.ts translate forge-specific payloads to this shared shape. The existing Forgejo handler is unchanged; GitHub/GitLab routes normalise then dispatch through dispatchForgeEvent using the same handler set.

Test plan

  • just qa passes — typecheck + Biome lint/format clean
  • 51 normaliser tests pass (webhook-normalize.test.ts) — all three forges produce identical ForgeEvent for equivalent actions; signature-fail scenarios per forge return 403; unknown events return 204; unknown repos return 404
  • Existing 48 webhook tests unchanged

Closes #294

Three new routes `/webhooks/forgejo`, `/webhooks/github`, `/webhooks/gitlab` with forge-specific signature verification (HMAC-SHA256 for Forgejo/GitHub, static token equality for GitLab). Legacy `/webhooks` alias kept indefinitely. Unknown events return `204`; signature failures return `403`. New `ForgeEvent` discriminated union in `@claude-hooks/shared` covers all events the Forgejo dispatch currently handles. Pure normaliser functions (`normalizeForgejoPayload`, `normalizeGitHubPayload`, `normalizeGitLabPayload`) in `webhook-normalize.ts` translate forge-specific payloads to this shared shape. The existing Forgejo handler is unchanged; GitHub/GitLab routes normalise then dispatch through `dispatchForgeEvent` using the same handler set. ## Test plan - [ ] `just qa` passes — typecheck + Biome lint/format clean - [ ] 51 normaliser tests pass (`webhook-normalize.test.ts`) — all three forges produce identical `ForgeEvent` for equivalent actions; signature-fail scenarios per forge return `403`; unknown events return `204`; unknown repos return `404` - [ ] Existing 48 webhook tests unchanged Closes #294
feat(webhook): per-forge webhook ingress + signature verification (MF-3)
All checks were successful
qa / qa (pull_request) Successful in 4m20s
qa / dockerfile (pull_request) Successful in 9s
056899c8b2
Three new routes /webhooks/forgejo, /webhooks/github, /webhooks/gitlab with
forge-specific signature verification (HMAC-SHA256 for Forgejo/GitHub,
static token for GitLab). Legacy /webhooks alias kept for back-compat.

Adds ForgeEvent discriminated union in @claude-hooks/shared and pure
normaliser functions (normalizeForgejoPayload, normalizeGitHubPayload,
normalizeGitLabPayload) in webhook-normalize.ts. Unknown events return
204; signature failures return 403.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dev requested review from reviewer 2026-04-24 00:06:21 +00:00
fix(webhook): address review for per-forge ingress (#307 follow-up)
All checks were successful
qa / qa (pull_request) Successful in 3m37s
qa / dockerfile (pull_request) Successful in 7s
de81c718aa
Minor hygiene + doc pass on top of the per-forge webhook ingress:

- Drop unused `afterEach` / `mock` imports from webhook-normalize.test.ts
  (Biome doesn't catch these but they misrepresent the test harness).
- Expand the `label_updated` comment in `normalizeForgejoPayload` so
  callers know the first-label pick is deterministic-but-lossy and they
  must iterate `issue.labels` themselves if per-label routing matters
  (matches the semantic the existing Forgejo dispatch switch in
  webhook.ts already uses).

No behaviour change. All 99 webhook + normalize tests still pass;
pre-existing theme / session-pruning / foreman failures unrelated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
code-lead closed this pull request 2026-04-24 08:47:47 +00:00
Collaborator

Closed as duplicate of #305. Per MF-3 review: #305 uses per-forge webhook secrets (githubSecret/gitlabSecret) which satisfies spec § MF-3 requirement that per-forge secrets be unique to limit blast radius. #307 reuses cfg.secret across all three forges — a leaked Forgejo HMAC would double as a GitLab-webhook impersonation token.

Closed as duplicate of #305. Per MF-3 review: #305 uses per-forge webhook secrets (githubSecret/gitlabSecret) which satisfies spec § MF-3 requirement that per-forge secrets be unique to limit blast radius. #307 reuses cfg.secret across all three forges — a leaked Forgejo HMAC would double as a GitLab-webhook impersonation token.
All checks were successful
qa / qa (pull_request) Successful in 3m37s
Required
Details
qa / dockerfile (pull_request) Successful in 7s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
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!307
No description provided.