feat(rotate): F5-rotate — operator-initiated + scheduled webhook secret rotation #504
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!504
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "dev/487"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements #487 end-to-end.
POST /watched-repos/:owner/:name/rotate-secret— generates a fresh 32-byte hex secret, calls the forge adapter'seditWebhook, persists the new secret; migrated rows (webhook_id IS NULL) auto-discover the right hook vialistWebhooksbefore rotatingGET /watched-repos— lists allwatched_reposrows for the settings UIwatched_reposSQLite table — seeded at boot fromrepoBindings; per-repo HMAC key overrides the global secret on incoming webhook verification for Forgejo, GitHub, and GitLabwebhook_id IS NOT NULLandupdated_atis older thaninterval_days; log format:[rotate] forge=<t> repo=<o/n> reason=auto age_days=<n>agents.jsonschema —webhook_secret_rotation { enabled, interval_days }; boot validation rejectsinterval_days < 7listWebhooks+editWebhookimplemented on Forgejo, GitHub, and GitLab adaptersTest plan
watched_reposentry seeded from config — verify row appears inGET /watched-reposPOST /watched-repos/:owner/:name/rotate-secreton a managed row (webhook_idset) — verify forge webhook is updated and new secret accepted on next deliverywebhook_id IS NULL) — verifylistWebhooksdiscovery, rotation, andwebhook_idbackfillwebhook_secret_rotationinagents.jsonwithinterval_days: 7— confirm hourly tick fires and rotates eligible rows with correct log lineinterval_days: 6— verify boot rejects with validation errorCloses #487
🤖 Generated with Claude Code
- 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>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 >= 7at parse time; Settings UI shows the red-dot/tooltip for migrated rows with per-row loading state and toast feedback.Nit:
webhook-secret-rotator.tssilently continues oneditWebhook → falsewith 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.09f4fba4447666fa64b47666fa64b40ed1ed768b0ed1ed768b8fc52a04d98fc52a04d90718ae640f0718ae640fa250b982b5a250b982b5f79bbb0f3bf79bbb0f3b0906a8572e0906a8572e49740dfd2849740dfd28d10447c0c7d10447c0c7cd02dd98c4