feat(webhook): watched_repos table + ingress switch + boot migration (#485) #506
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!506
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/485"
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
Replace the in-memory
agents.json#/reposwhitelist with a SQLitewatched_repostable keyed on(forge_type, owner, name). Per-repo HMAC secrets live on the row (webhook_secret); migrated rows fall back to the shared per-forge secret file viawebhook_id IS NULL.infrastructure/database/db.ts):watched_repostable withgetWatchedRepo,listWatchedRepos,listActiveWatchedRepos,upsertWatchedRepo,disableWatchedRepo,getActiveForgeRaw.shared/config/webhook-config.ts): one-shot — whenwatched_reposis empty, copyagents.json#/reposin, atomically rewriteagents.jsonwithoutrepos, write a.pre-f1.bakbackup, log[f1-migration] copied N repos from agents.json; backup at …. Subsequent boots that findrepos:reappearing log a warning and ignore it.http/webhook.ts): all three forge handlers look up the watched-repo row before verifying the signature/token (so per-repo secrets work) and checkservice_settings.active_forge_type— inactive forges short-circuit to 202 Accepted. GitHub + GitLab routes verify auth BEFORE the unhandled-event 204 short-circuit so an unrecognised event header can't bypass the auth gate.isKnownRepo(forge, owner, name)now readswatched_reposdirectly. Empty table → reject (no back-compat).Closes #485
Test plan
bun test apps/server/src/{infrastructure/database/db,shared/config/webhook-config,http/webhook,http/webhook-normalize,http/forge-event-broadcast,domain/workflow/event-handlers}.test.ts— 291 pass / 0 failbun x turbo run typecheck— green across all 4 workspacesbun x @biomejs/biome@^2 check .— only unrelated infosrepos:warns + ignoresforge=<type> repo=<owner/name>log🤖 Generated with Claude Code
All acceptance criteria from #485 verified, CI green, test coverage solid.
watched_reposschema + helpers correct;upsertWatchedRepoupdate semantics (undefined = preserve, null = clear) properly tested.pickWebhookSecret: F5-registered rows (webhook_id != null) correctly reject the shared-secret fallback; migrated rows (webhook_id IS NULL) fall through to the live config. Security property tested explicitly.behavior (nit, not blocking):
runWatchedReposBootMigration(webhook-config.ts~line 2143) iterates therepoBindingsloop callingupsertWatchedRepowithout wrapping in a SQLite transaction. The JSDoc states "a write error throws and aborts boot rather than leaving the table half-populated" — but that guarantee requires the inserts to be atomic. A genuine DB failure mid-loop (disk full etc.) would leave N-of-M repos inserted; next boot seesexisting.length > 0, skips the migration, and the remaining repos are silently lost. Fix: wrap the loop ingetDb().transaction(() => { for (const b of repoBindings) upsertWatchedRepo({…}); })(). The comment should then be accurate.test-gap (nit, not blocking):
handleGitHubWebhookandhandleGitLabWebhookhave no direct test coverage for the F4 per-repo secret path or active-forge 202 gate. The Forgejo suite covers every case exhaustively; the GH/GL paths follow the same pattern, so correctness is high — but the auth-before-204 ordering (a security property) goes un-exercised for those two routes. Worth adding in a follow-up.d1eff02684af9307056b