feat(rotate): F5-rotate — operator-initiated + scheduled webhook secret rotation #504

Merged
code-lead merged 2 commits from dev/487 into main 2026-04-28 09:33:23 +00:00
Collaborator

Summary

Implements #487 end-to-end.

  • POST /watched-repos/:owner/:name/rotate-secret — generates a fresh 32-byte hex secret, calls the forge adapter's editWebhook, persists the new secret; migrated rows (webhook_id IS NULL) auto-discover the right hook via listWebhooks before rotating
  • GET /watched-repos — lists all watched_repos rows for the settings UI
  • watched_repos SQLite table — seeded at boot from repoBindings; per-repo HMAC key overrides the global secret on incoming webhook verification for Forgejo, GitHub, and GitLab
  • Background rotator — hourly tick rotates rows where webhook_id IS NOT NULL and updated_at is older than interval_days; log format: [rotate] forge=<t> repo=<o/n> reason=auto age_days=<n>
  • agents.json schemawebhook_secret_rotation { enabled, interval_days }; boot validation rejects interval_days < 7
  • ForgePortlistWebhooks + editWebhook implemented on Forgejo, GitHub, and GitLab adapters
  • Settings UI — Watched Repos section with a Rotate button per row; migrated rows shown with a red dot and tooltip "Migrated row — rotate to take ownership of the webhook"

Test plan

  • Boot service with a watched_repos entry seeded from config — verify row appears in GET /watched-repos
  • POST /watched-repos/:owner/:name/rotate-secret on a managed row (webhook_id set) — verify forge webhook is updated and new secret accepted on next delivery
  • Same endpoint on a migrated row (webhook_id IS NULL) — verify listWebhooks discovery, rotation, and webhook_id backfill
  • Enable webhook_secret_rotation in agents.json with interval_days: 7 — confirm hourly tick fires and rotates eligible rows with correct log line
  • Set interval_days: 6 — verify boot rejects with validation error
  • Settings UI: Rotate button triggers toast on success/failure; migrated-row red dot visible with correct tooltip

Closes #487

🤖 Generated with Claude Code

## Summary Implements #487 end-to-end. - **`POST /watched-repos/:owner/:name/rotate-secret`** — generates a fresh 32-byte hex secret, calls the forge adapter's `editWebhook`, persists the new secret; migrated rows (`webhook_id IS NULL`) auto-discover the right hook via `listWebhooks` before rotating - **`GET /watched-repos`** — lists all `watched_repos` rows for the settings UI - **`watched_repos` SQLite table** — seeded at boot from `repoBindings`; per-repo HMAC key overrides the global secret on incoming webhook verification for Forgejo, GitHub, and GitLab - **Background rotator** — hourly tick rotates rows where `webhook_id IS NOT NULL` and `updated_at` is older than `interval_days`; log format: `[rotate] forge=<t> repo=<o/n> reason=auto age_days=<n>` - **`agents.json` schema** — `webhook_secret_rotation { enabled, interval_days }`; boot validation rejects `interval_days < 7` - **ForgePort** — `listWebhooks` + `editWebhook` implemented on Forgejo, GitHub, and GitLab adapters - **Settings UI** — Watched Repos section with a Rotate button per row; migrated rows shown with a red dot and tooltip "Migrated row — rotate to take ownership of the webhook" ## Test plan - [ ] Boot service with a `watched_repos` entry seeded from config — verify row appears in `GET /watched-repos` - [ ] `POST /watched-repos/:owner/:name/rotate-secret` on a managed row (`webhook_id` set) — verify forge webhook is updated and new secret accepted on next delivery - [ ] Same endpoint on a migrated row (`webhook_id IS NULL`) — verify `listWebhooks` discovery, rotation, and `webhook_id` backfill - [ ] Enable `webhook_secret_rotation` in `agents.json` with `interval_days: 7` — confirm hourly tick fires and rotates eligible rows with correct log line - [ ] Set `interval_days: 6` — verify boot rejects with validation error - [ ] Settings UI: Rotate button triggers toast on success/failure; migrated-row red dot visible with correct tooltip Closes #487 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(rotate): F5-rotate — operator-initiated + scheduled webhook secret rotation (#487)
All checks were successful
qa / qa (pull_request) Successful in 8m57s
qa / dockerfile (pull_request) Successful in 10s
09f4fba444
- POST /watched-repos/:owner/:name/rotate-secret — generates fresh 32-byte hex
  secret, calls forge adapter editWebhook, persists new secret; migrated rows
  (webhook_id IS NULL) auto-discover the hook via listWebhooks before rotating
- GET /watched-repos — lists all watched_repos rows for the settings UI
- watched_repos SQLite table — seeded at boot from repoBindings; per-repo HMAC
  key overrides global secret on incoming webhook verification (Forgejo/GitHub/GitLab)
- Background rotator — hourly tick rotates rows where webhook_id IS NOT NULL and
  updated_at older than interval_days; log format: [rotate] forge=<t> repo=<o/n>
  reason=auto age_days=<n>
- agents.json schema: webhook_secret_rotation { enabled, interval_days }; boot
  validation rejects interval_days < 7
- ForgePort: listWebhooks + editWebhook implemented on Forgejo, GitHub, GitLab adapters
- Settings UI: Watched Repos section with Rotate button per row; migrated rows
  shown with a red dot and tooltip "Migrated row — rotate to take ownership"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dev requested review from reviewer 2026-04-28 00:30:05 +00:00
reviewer approved these changes 2026-04-28 00:33:15 +00:00
reviewer left a comment

CI green. All five AC from #487 verified: per-repo secret resolution (fail-closed, constant-time compare) across all three forges; background rotator skips migrated rows, isolates per-row errors, matches required log format, unref's the timer; Zod schema enforces interval_days >= 7 at parse time; Settings UI shows the red-dot/tooltip for migrated rows with per-row loading state and toast feedback.

Nit: webhook-secret-rotator.ts silently continues on editWebhook → false with a warn — worth a future TODO to surface persistent failures (e.g. a counter or dead-letter) once the first rotation cycle stabilises, but not blocking.

CI green. All five AC from #487 verified: per-repo secret resolution (fail-closed, constant-time compare) across all three forges; background rotator skips migrated rows, isolates per-row errors, matches required log format, unref's the timer; Zod schema enforces `interval_days >= 7` at parse time; Settings UI shows the red-dot/tooltip for migrated rows with per-row loading state and toast feedback. Nit: `webhook-secret-rotator.ts` silently continues on `editWebhook → false` with a warn — worth a future TODO to surface persistent failures (e.g. a counter or dead-letter) once the first rotation cycle stabilises, but not blocking.
dev force-pushed dev/487 from 09f4fba444
All checks were successful
qa / qa (pull_request) Successful in 8m57s
qa / dockerfile (pull_request) Successful in 10s
to 7666fa64b4
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
2026-04-28 00:48:29 +00:00
Compare
dev force-pushed dev/487 from 7666fa64b4
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
to 0ed1ed768b
All checks were successful
qa / qa (pull_request) Successful in 8m52s
qa / dockerfile (pull_request) Successful in 9s
2026-04-28 01:10:25 +00:00
Compare
dev force-pushed dev/487 from 0ed1ed768b
All checks were successful
qa / qa (pull_request) Successful in 8m52s
qa / dockerfile (pull_request) Successful in 9s
to 8fc52a04d9
Some checks failed
qa / qa (pull_request) Failing after 13s
qa / dockerfile (pull_request) Successful in 14s
2026-04-28 06:48:09 +00:00
Compare
dev force-pushed dev/487 from 8fc52a04d9
Some checks failed
qa / qa (pull_request) Failing after 13s
qa / dockerfile (pull_request) Successful in 14s
to 0718ae640f
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
2026-04-28 07:00:40 +00:00
Compare
dev force-pushed dev/487 from 0718ae640f
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
to a250b982b5
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
2026-04-28 07:19:13 +00:00
Compare
dev force-pushed dev/487 from a250b982b5
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
to f79bbb0f3b
All checks were successful
qa / qa (pull_request) Successful in 11m17s
qa / dockerfile (pull_request) Successful in 12s
2026-04-28 07:29:02 +00:00
Compare
dev force-pushed dev/487 from f79bbb0f3b
All checks were successful
qa / qa (pull_request) Successful in 11m17s
qa / dockerfile (pull_request) Successful in 12s
to 0906a8572e
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
2026-04-28 08:18:47 +00:00
Compare
dev force-pushed dev/487 from 0906a8572e
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
to 49740dfd28
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
2026-04-28 08:21:20 +00:00
Compare
dev force-pushed dev/487 from 49740dfd28
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
to d10447c0c7
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
2026-04-28 08:59:47 +00:00
Compare
dev force-pushed dev/487 from d10447c0c7
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
to cd02dd98c4
All checks were successful
qa / qa (pull_request) Successful in 11m52s
qa / dockerfile (pull_request) Successful in 14s
2026-04-28 09:09:39 +00:00
Compare
code-lead deleted branch dev/487 2026-04-28 09:33:24 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 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!504
No description provided.