feat: add webhook endpoint admin API, in-memory store, config.json sync, and per-endpoint delivery tuning - #5251
Conversation
|
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
📝 WalkthroughSummary by CodeRabbit
WalkthroughWebhook support adds persisted client settings, declarative endpoint loading, in-memory endpoint indexes, administrative HTTP routes, delivery history and redelivery operations, schema definitions, and dispatcher lifecycle wiring. ChangesWebhook platform
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant ConfigFile
participant Config
participant ConfigStore
participant WebhookHandler
participant WebhookDispatcher
ConfigFile->>Config: load webhook declarations
Config->>ConfigStore: reconcile endpoint rows
ConfigStore-->>Config: reload canonical endpoints
Config->>WebhookDispatcher: replace runtime endpoints
WebhookHandler->>ConfigStore: persist endpoint changes
WebhookHandler->>WebhookDispatcher: request test delivery
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Comment |
Confidence Score: 5/5The latest fixes look safe to merge.
Important Files Changed
Reviews (6): Last reviewed commit: "feat: webhook admin API, in-memory endpo..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
transports/bifrost-http/lib/config.go (1)
6297-6317: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win
SetWebhookEndpoint's shallow copy aliasesEvents/Headerswith the caller's struct.
copied := *endpointcopies the pointer/slice/map fields by reference. Callers must currently avoid mutatingEvents/Headerson the struct after callingSetWebhookEndpoint, but nothing enforces this, and only theNamefield's immutability is tested (TestWebhookEndpointMemoryStore). SinceWebhookEndpointByID/ByNamepromise read-only access, a future caller mutating a reusedendpoint.Headersmap post-Set(e.g. for redaction) would corrupt the cache the delivery worker reads, without any lock protecting that mutation. As per coding guidelines, "mutable config maps such asExtraHeadersmust be defensively copied."🔒 Proposed fix: defensively copy Events/Headers
copied := *endpoint + if endpoint.Events != nil { + copied.Events = append([]configstoreTables.WebhookEvent(nil), endpoint.Events...) + } + if endpoint.Headers != nil { + copied.Headers = make(map[string]schemas.SecretVar, len(endpoint.Headers)) + for k, v := range endpoint.Headers { + copied.Headers[k] = v + } + } c.muWebhooks.Lock()🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@transports/bifrost-http/lib/config.go` around lines 6297 - 6317, Update SetWebhookEndpoint to deep-copy the mutable Events and Headers fields when creating the cached endpoint, rather than relying on the shallow copied struct. Preserve the existing ID/name replacement and indexing behavior while ensuring later caller mutations cannot alter values returned by WebhookEndpointByID or WebhookEndpointByName.Source: Coding guidelines
transports/config.schema.json (1)
1285-1292: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winSchema allows empty
events, but the admin API rejects it.
eventshas nominItems, so a config.json declaration with"events": []passes schema validation, while the same payload sent through the admin API (webhooks.go, perTestWebhookHandlerCreateValidation's "no events" case) is rejected with 400. Add"minItems": 1here so config.json-declared webhooks fail fast at schema validation instead of only at reconciliation time.♻️ Proposed fix
"events": { "type": "array", "items": { "type": "string", "enum": ["async_job.completed", "async_job.failed"] }, + "minItems": 1, "description": "Events this endpoint subscribes to." },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@transports/config.schema.json` around lines 1285 - 1292, Update the events array schema in the webhook endpoint definition to require at least one item by adding minItems: 1 alongside its existing type, items, and description properties. Preserve the current event enum and array validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@transports/bifrost-http/handlers/webhooks.go`:
- Around line 265-284: Update deleteWebhookEndpoint to remove the deleted
endpoint’s id from h.lastTestFire after DeleteWebhookEndpoint succeeds,
alongside h.store.RemoveWebhookEndpoint(id). Ensure cleanup occurs only after a
successful deletion while preserving the existing error responses.
In `@transports/bifrost-http/lib/config.go`:
- Around line 2101-2155: Update syncWebhookEndpointsFromFile to resolve the
existing endpoint match by name or ID before calling
GenerateWebhookEndpointHash. If hash generation fails, mark the matched existing
endpoint ID in keepIDs before continuing, while leaving unmatched entries
uncreated and preserving the existing update/create behavior for successful
hashes.
---
Nitpick comments:
In `@transports/bifrost-http/lib/config.go`:
- Around line 6297-6317: Update SetWebhookEndpoint to deep-copy the mutable
Events and Headers fields when creating the cached endpoint, rather than relying
on the shallow copied struct. Preserve the existing ID/name replacement and
indexing behavior while ensuring later caller mutations cannot alter values
returned by WebhookEndpointByID or WebhookEndpointByName.
In `@transports/config.schema.json`:
- Around line 1285-1292: Update the events array schema in the webhook endpoint
definition to require at least one item by adding minItems: 1 alongside its
existing type, items, and description properties. Preserve the current event
enum and array validation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 781f0aab-2586-4a8a-a96b-b3248367eb69
📒 Files selected for processing (11)
framework/configstore/clientconfig.goframework/configstore/migrations.goframework/configstore/rdb.goframework/configstore/rdb_test.goframework/configstore/tables/clientconfig.gotransports/bifrost-http/handlers/webhooks.gotransports/bifrost-http/handlers/webhooks_test.gotransports/bifrost-http/lib/config.gotransports/bifrost-http/lib/config_test.gotransports/bifrost-http/server/server.gotransports/config.schema.json
bd3a160 to
3da8f9d
Compare
b3c59cc to
181e27d
Compare
181e27d to
e946051
Compare
3da8f9d to
b37f4a7
Compare
b37f4a7 to
ef2e911
Compare
e946051 to
08273ac
Compare
ef2e911 to
cf1347e
Compare
Merge activity
|
The base branch was changed.
cf1347e to
6a7c2aa
Compare
…nc, and per-endpoint delivery tuning (#5251) This PR wires up the webhook admin API and integrates webhook endpoint configuration into the config.json lifecycle. It adds a `WebhookConfig` global settings struct to the client config, a `webhooks` section to config.json for declaring endpoints declaratively, an in-memory endpoint store on `Config` for zero-database-read serving on the hot path, and a full set of admin HTTP handlers for endpoint CRUD, secret rotation, test delivery, delivery history, and redelivery. - Added `WebhookConfig` to `ClientConfig` and `TableClientConfig`, stored as a JSON blob column (`webhook_config_json`) with `BeforeSave`/`AfterFind` serialization. Includes a `DeliveryHistoryRetention()` helper with a 30-day default on a nil receiver. - Added a database migration (`add_webhook_config_client_column`) to introduce the column. - Added `GenerateWebhookEndpointHash` for deterministic change detection of endpoint declarations, covering all declared fields (sorted events and headers for stability) while excluding operational counters and generated IDs. - Added `WebhookEndpointConfig` to `ConfigData` and the `loadWebhooksConfig` startup step, which validates declarations with a warn-and-skip policy and reconciles them against the database using either an additive merge or a full sync depending on whether `source_of_truth: config.json` is set and the `webhooks` section is physically present. - Added an in-memory webhook endpoint store (`webhookEndpoints`, `webhookEndpointsByName`) on `Config`, guarded by a dedicated `muWebhooks` mutex, with `WebhookEndpointByID`, `WebhookEndpointByName`, `SetWebhookEndpoint`, `RemoveWebhookEndpoint`, and `replaceWebhookEndpoints` methods. Handlers keep it in lockstep with every database write. - Added `WebhookHandler` with routes for list, get, create, update, delete, rotate-secret, test delivery (with a per-endpoint 10-second cooldown), delivery history pagination, and redelivery. Custom header values are redacted in all API responses; masked placeholders round-tripped through an update are restored from the in-memory store to avoid corrupting stored values. - Wired `WebhookDispatcher` into `BifrostHTTPServer.Bootstrap`, passing it to the async job executor and stopping it on shutdown. The dispatcher is only initialized when both the config store and logs store are available. - Extended `config.schema.json` with the `webhooks` array and `client.webhook_config` object schemas. Notable design decisions: - The in-memory store deliberately does not mirror operational counters (failure counts, timestamps); list views that need them read the database directly. - Config hash for `WebhookConfig` is only written when the field is non-nil to avoid hash churn on upgrade for existing deployments. - Pruning in source-of-truth mode requires the `webhooks` key to be physically present in the file, preventing an absent section from wiping all database endpoints. - The typed-nil dispatcher problem for the async job executor interface is handled explicitly to avoid a non-nil interface wrapping a nil pointer. - [ ] Bug fix - [x] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI - [x] Core (Go) - [x] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [ ] Docs ```sh go test ./framework/configstore/... ./transports/bifrost-http/... ``` - Create a webhook endpoint via `POST /api/webhooks` and verify the secret is returned exactly once. - Rotate the secret via `POST /api/webhooks/{id}/rotate-secret` and confirm the old secret is no longer used for signing. - Add a `webhooks` section to config.json and restart; verify the endpoint appears in the database and is served from memory. - Set `source_of_truth: config.json`, remove an endpoint from the file, and restart; verify the database row is pruned. - Set `source_of_truth: config.json` without a `webhooks` key; verify existing database endpoints are not pruned. - Fire a test delivery via `POST /api/webhooks/{id}/test` and confirm the receiver gets a signed request; fire again immediately and confirm a 429 response. - Seed a failed delivery and redeliver via `POST /api/webhooks/deliveries/{id}/redeliver`; confirm the job appears in the queue under the original webhook ID and a second redeliver returns 409. New config fields: | Field | Location | Description | |---|---|---| | `client.webhook_config.delivery_history_retention_days` | config.json `client` block | How long delivery history rows are kept (default: 30 days) | | `webhooks[].max_retries` | config.json `webhooks` array | Per-endpoint retry count (default: 4) | | `webhooks[].attempt_timeout_seconds` | config.json `webhooks` array | Per-attempt timeout (default: 10s) | | `webhooks[].max_concurrent_deliveries` | config.json `webhooks` array | Concurrency cap per node (default: 10) | - [ ] Yes - [x] No - Signing secrets are generated server-side and returned exactly once at creation or rotation; they are never included in read responses or list views. - Custom header values (e.g. `Authorization`) are encrypted at rest and redacted in all API responses; only header names are visible. - Reserved delivery headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`, `content-type`) cannot be overridden by callers. - Plain HTTP delivery URLs require explicit `allow_private_network: true`; link-local and metadata addresses are always blocked regardless of that flag. - The per-endpoint test delivery cooldown (10 seconds) limits abuse of the test endpoint against external receivers. - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [x] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
…nc, and per-endpoint delivery tuning (#5251) This PR wires up the webhook admin API and integrates webhook endpoint configuration into the config.json lifecycle. It adds a `WebhookConfig` global settings struct to the client config, a `webhooks` section to config.json for declaring endpoints declaratively, an in-memory endpoint store on `Config` for zero-database-read serving on the hot path, and a full set of admin HTTP handlers for endpoint CRUD, secret rotation, test delivery, delivery history, and redelivery. - Added `WebhookConfig` to `ClientConfig` and `TableClientConfig`, stored as a JSON blob column (`webhook_config_json`) with `BeforeSave`/`AfterFind` serialization. Includes a `DeliveryHistoryRetention()` helper with a 30-day default on a nil receiver. - Added a database migration (`add_webhook_config_client_column`) to introduce the column. - Added `GenerateWebhookEndpointHash` for deterministic change detection of endpoint declarations, covering all declared fields (sorted events and headers for stability) while excluding operational counters and generated IDs. - Added `WebhookEndpointConfig` to `ConfigData` and the `loadWebhooksConfig` startup step, which validates declarations with a warn-and-skip policy and reconciles them against the database using either an additive merge or a full sync depending on whether `source_of_truth: config.json` is set and the `webhooks` section is physically present. - Added an in-memory webhook endpoint store (`webhookEndpoints`, `webhookEndpointsByName`) on `Config`, guarded by a dedicated `muWebhooks` mutex, with `WebhookEndpointByID`, `WebhookEndpointByName`, `SetWebhookEndpoint`, `RemoveWebhookEndpoint`, and `replaceWebhookEndpoints` methods. Handlers keep it in lockstep with every database write. - Added `WebhookHandler` with routes for list, get, create, update, delete, rotate-secret, test delivery (with a per-endpoint 10-second cooldown), delivery history pagination, and redelivery. Custom header values are redacted in all API responses; masked placeholders round-tripped through an update are restored from the in-memory store to avoid corrupting stored values. - Wired `WebhookDispatcher` into `BifrostHTTPServer.Bootstrap`, passing it to the async job executor and stopping it on shutdown. The dispatcher is only initialized when both the config store and logs store are available. - Extended `config.schema.json` with the `webhooks` array and `client.webhook_config` object schemas. Notable design decisions: - The in-memory store deliberately does not mirror operational counters (failure counts, timestamps); list views that need them read the database directly. - Config hash for `WebhookConfig` is only written when the field is non-nil to avoid hash churn on upgrade for existing deployments. - Pruning in source-of-truth mode requires the `webhooks` key to be physically present in the file, preventing an absent section from wiping all database endpoints. - The typed-nil dispatcher problem for the async job executor interface is handled explicitly to avoid a non-nil interface wrapping a nil pointer. - [ ] Bug fix - [x] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI - [x] Core (Go) - [x] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [ ] Docs ```sh go test ./framework/configstore/... ./transports/bifrost-http/... ``` - Create a webhook endpoint via `POST /api/webhooks` and verify the secret is returned exactly once. - Rotate the secret via `POST /api/webhooks/{id}/rotate-secret` and confirm the old secret is no longer used for signing. - Add a `webhooks` section to config.json and restart; verify the endpoint appears in the database and is served from memory. - Set `source_of_truth: config.json`, remove an endpoint from the file, and restart; verify the database row is pruned. - Set `source_of_truth: config.json` without a `webhooks` key; verify existing database endpoints are not pruned. - Fire a test delivery via `POST /api/webhooks/{id}/test` and confirm the receiver gets a signed request; fire again immediately and confirm a 429 response. - Seed a failed delivery and redeliver via `POST /api/webhooks/deliveries/{id}/redeliver`; confirm the job appears in the queue under the original webhook ID and a second redeliver returns 409. New config fields: | Field | Location | Description | |---|---|---| | `client.webhook_config.delivery_history_retention_days` | config.json `client` block | How long delivery history rows are kept (default: 30 days) | | `webhooks[].max_retries` | config.json `webhooks` array | Per-endpoint retry count (default: 4) | | `webhooks[].attempt_timeout_seconds` | config.json `webhooks` array | Per-attempt timeout (default: 10s) | | `webhooks[].max_concurrent_deliveries` | config.json `webhooks` array | Concurrency cap per node (default: 10) | - [ ] Yes - [x] No - Signing secrets are generated server-side and returned exactly once at creation or rotation; they are never included in read responses or list views. - Custom header values (e.g. `Authorization`) are encrypted at rest and redacted in all API responses; only header names are visible. - Reserved delivery headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`, `content-type`) cannot be overridden by callers. - Plain HTTP delivery URLs require explicit `allow_private_network: true`; link-local and metadata addresses are always blocked regardless of that flag. - The per-endpoint test delivery cooldown (10 seconds) limits abuse of the test endpoint against external receivers. - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [x] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
…nc, and per-endpoint delivery tuning (maximhq#5251) This PR wires up the webhook admin API and integrates webhook endpoint configuration into the config.json lifecycle. It adds a `WebhookConfig` global settings struct to the client config, a `webhooks` section to config.json for declaring endpoints declaratively, an in-memory endpoint store on `Config` for zero-database-read serving on the hot path, and a full set of admin HTTP handlers for endpoint CRUD, secret rotation, test delivery, delivery history, and redelivery. - Added `WebhookConfig` to `ClientConfig` and `TableClientConfig`, stored as a JSON blob column (`webhook_config_json`) with `BeforeSave`/`AfterFind` serialization. Includes a `DeliveryHistoryRetention()` helper with a 30-day default on a nil receiver. - Added a database migration (`add_webhook_config_client_column`) to introduce the column. - Added `GenerateWebhookEndpointHash` for deterministic change detection of endpoint declarations, covering all declared fields (sorted events and headers for stability) while excluding operational counters and generated IDs. - Added `WebhookEndpointConfig` to `ConfigData` and the `loadWebhooksConfig` startup step, which validates declarations with a warn-and-skip policy and reconciles them against the database using either an additive merge or a full sync depending on whether `source_of_truth: config.json` is set and the `webhooks` section is physically present. - Added an in-memory webhook endpoint store (`webhookEndpoints`, `webhookEndpointsByName`) on `Config`, guarded by a dedicated `muWebhooks` mutex, with `WebhookEndpointByID`, `WebhookEndpointByName`, `SetWebhookEndpoint`, `RemoveWebhookEndpoint`, and `replaceWebhookEndpoints` methods. Handlers keep it in lockstep with every database write. - Added `WebhookHandler` with routes for list, get, create, update, delete, rotate-secret, test delivery (with a per-endpoint 10-second cooldown), delivery history pagination, and redelivery. Custom header values are redacted in all API responses; masked placeholders round-tripped through an update are restored from the in-memory store to avoid corrupting stored values. - Wired `WebhookDispatcher` into `BifrostHTTPServer.Bootstrap`, passing it to the async job executor and stopping it on shutdown. The dispatcher is only initialized when both the config store and logs store are available. - Extended `config.schema.json` with the `webhooks` array and `client.webhook_config` object schemas. Notable design decisions: - The in-memory store deliberately does not mirror operational counters (failure counts, timestamps); list views that need them read the database directly. - Config hash for `WebhookConfig` is only written when the field is non-nil to avoid hash churn on upgrade for existing deployments. - Pruning in source-of-truth mode requires the `webhooks` key to be physically present in the file, preventing an absent section from wiping all database endpoints. - The typed-nil dispatcher problem for the async job executor interface is handled explicitly to avoid a non-nil interface wrapping a nil pointer. - [ ] Bug fix - [x] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI - [x] Core (Go) - [x] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [ ] Docs ```sh go test ./framework/configstore/... ./transports/bifrost-http/... ``` - Create a webhook endpoint via `POST /api/webhooks` and verify the secret is returned exactly once. - Rotate the secret via `POST /api/webhooks/{id}/rotate-secret` and confirm the old secret is no longer used for signing. - Add a `webhooks` section to config.json and restart; verify the endpoint appears in the database and is served from memory. - Set `source_of_truth: config.json`, remove an endpoint from the file, and restart; verify the database row is pruned. - Set `source_of_truth: config.json` without a `webhooks` key; verify existing database endpoints are not pruned. - Fire a test delivery via `POST /api/webhooks/{id}/test` and confirm the receiver gets a signed request; fire again immediately and confirm a 429 response. - Seed a failed delivery and redeliver via `POST /api/webhooks/deliveries/{id}/redeliver`; confirm the job appears in the queue under the original webhook ID and a second redeliver returns 409. New config fields: | Field | Location | Description | |---|---|---| | `client.webhook_config.delivery_history_retention_days` | config.json `client` block | How long delivery history rows are kept (default: 30 days) | | `webhooks[].max_retries` | config.json `webhooks` array | Per-endpoint retry count (default: 4) | | `webhooks[].attempt_timeout_seconds` | config.json `webhooks` array | Per-attempt timeout (default: 10s) | | `webhooks[].max_concurrent_deliveries` | config.json `webhooks` array | Concurrency cap per node (default: 10) | - [ ] Yes - [x] No - Signing secrets are generated server-side and returned exactly once at creation or rotation; they are never included in read responses or list views. - Custom header values (e.g. `Authorization`) are encrypted at rest and redacted in all API responses; only header names are visible. - Reserved delivery headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`, `content-type`) cannot be overridden by callers. - Plain HTTP delivery URLs require explicit `allow_private_network: true`; link-local and metadata addresses are always blocked regardless of that flag. - The per-endpoint test delivery cooldown (10 seconds) limits abuse of the test endpoint against external receivers. - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [x] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
…nc, and per-endpoint delivery tuning (maximhq#5251) This PR wires up the webhook admin API and integrates webhook endpoint configuration into the config.json lifecycle. It adds a `WebhookConfig` global settings struct to the client config, a `webhooks` section to config.json for declaring endpoints declaratively, an in-memory endpoint store on `Config` for zero-database-read serving on the hot path, and a full set of admin HTTP handlers for endpoint CRUD, secret rotation, test delivery, delivery history, and redelivery. - Added `WebhookConfig` to `ClientConfig` and `TableClientConfig`, stored as a JSON blob column (`webhook_config_json`) with `BeforeSave`/`AfterFind` serialization. Includes a `DeliveryHistoryRetention()` helper with a 30-day default on a nil receiver. - Added a database migration (`add_webhook_config_client_column`) to introduce the column. - Added `GenerateWebhookEndpointHash` for deterministic change detection of endpoint declarations, covering all declared fields (sorted events and headers for stability) while excluding operational counters and generated IDs. - Added `WebhookEndpointConfig` to `ConfigData` and the `loadWebhooksConfig` startup step, which validates declarations with a warn-and-skip policy and reconciles them against the database using either an additive merge or a full sync depending on whether `source_of_truth: config.json` is set and the `webhooks` section is physically present. - Added an in-memory webhook endpoint store (`webhookEndpoints`, `webhookEndpointsByName`) on `Config`, guarded by a dedicated `muWebhooks` mutex, with `WebhookEndpointByID`, `WebhookEndpointByName`, `SetWebhookEndpoint`, `RemoveWebhookEndpoint`, and `replaceWebhookEndpoints` methods. Handlers keep it in lockstep with every database write. - Added `WebhookHandler` with routes for list, get, create, update, delete, rotate-secret, test delivery (with a per-endpoint 10-second cooldown), delivery history pagination, and redelivery. Custom header values are redacted in all API responses; masked placeholders round-tripped through an update are restored from the in-memory store to avoid corrupting stored values. - Wired `WebhookDispatcher` into `BifrostHTTPServer.Bootstrap`, passing it to the async job executor and stopping it on shutdown. The dispatcher is only initialized when both the config store and logs store are available. - Extended `config.schema.json` with the `webhooks` array and `client.webhook_config` object schemas. Notable design decisions: - The in-memory store deliberately does not mirror operational counters (failure counts, timestamps); list views that need them read the database directly. - Config hash for `WebhookConfig` is only written when the field is non-nil to avoid hash churn on upgrade for existing deployments. - Pruning in source-of-truth mode requires the `webhooks` key to be physically present in the file, preventing an absent section from wiping all database endpoints. - The typed-nil dispatcher problem for the async job executor interface is handled explicitly to avoid a non-nil interface wrapping a nil pointer. - [ ] Bug fix - [x] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI - [x] Core (Go) - [x] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [ ] Docs ```sh go test ./framework/configstore/... ./transports/bifrost-http/... ``` - Create a webhook endpoint via `POST /api/webhooks` and verify the secret is returned exactly once. - Rotate the secret via `POST /api/webhooks/{id}/rotate-secret` and confirm the old secret is no longer used for signing. - Add a `webhooks` section to config.json and restart; verify the endpoint appears in the database and is served from memory. - Set `source_of_truth: config.json`, remove an endpoint from the file, and restart; verify the database row is pruned. - Set `source_of_truth: config.json` without a `webhooks` key; verify existing database endpoints are not pruned. - Fire a test delivery via `POST /api/webhooks/{id}/test` and confirm the receiver gets a signed request; fire again immediately and confirm a 429 response. - Seed a failed delivery and redeliver via `POST /api/webhooks/deliveries/{id}/redeliver`; confirm the job appears in the queue under the original webhook ID and a second redeliver returns 409. New config fields: | Field | Location | Description | |---|---|---| | `client.webhook_config.delivery_history_retention_days` | config.json `client` block | How long delivery history rows are kept (default: 30 days) | | `webhooks[].max_retries` | config.json `webhooks` array | Per-endpoint retry count (default: 4) | | `webhooks[].attempt_timeout_seconds` | config.json `webhooks` array | Per-attempt timeout (default: 10s) | | `webhooks[].max_concurrent_deliveries` | config.json `webhooks` array | Concurrency cap per node (default: 10) | - [ ] Yes - [x] No - Signing secrets are generated server-side and returned exactly once at creation or rotation; they are never included in read responses or list views. - Custom header values (e.g. `Authorization`) are encrypted at rest and redacted in all API responses; only header names are visible. - Reserved delivery headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`, `content-type`) cannot be overridden by callers. - Plain HTTP delivery URLs require explicit `allow_private_network: true`; link-local and metadata addresses are always blocked regardless of that flag. - The per-endpoint test delivery cooldown (10 seconds) limits abuse of the test endpoint against external receivers. - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [x] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable

Summary
This PR wires up the webhook admin API and integrates webhook endpoint configuration into the config.json lifecycle. It adds a
WebhookConfigglobal settings struct to the client config, awebhookssection to config.json for declaring endpoints declaratively, an in-memory endpoint store onConfigfor zero-database-read serving on the hot path, and a full set of admin HTTP handlers for endpoint CRUD, secret rotation, test delivery, delivery history, and redelivery.Changes
WebhookConfigtoClientConfigandTableClientConfig, stored as a JSON blob column (webhook_config_json) withBeforeSave/AfterFindserialization. Includes aDeliveryHistoryRetention()helper with a 30-day default on a nil receiver.add_webhook_config_client_column) to introduce the column.GenerateWebhookEndpointHashfor deterministic change detection of endpoint declarations, covering all declared fields (sorted events and headers for stability) while excluding operational counters and generated IDs.WebhookEndpointConfigtoConfigDataand theloadWebhooksConfigstartup step, which validates declarations with a warn-and-skip policy and reconciles them against the database using either an additive merge or a full sync depending on whethersource_of_truth: config.jsonis set and thewebhookssection is physically present.webhookEndpoints,webhookEndpointsByName) onConfig, guarded by a dedicatedmuWebhooksmutex, withWebhookEndpointByID,WebhookEndpointByName,SetWebhookEndpoint,RemoveWebhookEndpoint, andreplaceWebhookEndpointsmethods. Handlers keep it in lockstep with every database write.WebhookHandlerwith routes for list, get, create, update, delete, rotate-secret, test delivery (with a per-endpoint 10-second cooldown), delivery history pagination, and redelivery. Custom header values are redacted in all API responses; masked placeholders round-tripped through an update are restored from the in-memory store to avoid corrupting stored values.WebhookDispatcherintoBifrostHTTPServer.Bootstrap, passing it to the async job executor and stopping it on shutdown. The dispatcher is only initialized when both the config store and logs store are available.config.schema.jsonwith thewebhooksarray andclient.webhook_configobject schemas.Notable design decisions:
WebhookConfigis only written when the field is non-nil to avoid hash churn on upgrade for existing deployments.webhookskey to be physically present in the file, preventing an absent section from wiping all database endpoints.Type of change
Affected areas
How to test
go test ./framework/configstore/... ./transports/bifrost-http/...POST /api/webhooksand verify the secret is returned exactly once.POST /api/webhooks/{id}/rotate-secretand confirm the old secret is no longer used for signing.webhookssection to config.json and restart; verify the endpoint appears in the database and is served from memory.source_of_truth: config.json, remove an endpoint from the file, and restart; verify the database row is pruned.source_of_truth: config.jsonwithout awebhookskey; verify existing database endpoints are not pruned.POST /api/webhooks/{id}/testand confirm the receiver gets a signed request; fire again immediately and confirm a 429 response.POST /api/webhooks/deliveries/{id}/redeliver; confirm the job appears in the queue under the original webhook ID and a second redeliver returns 409.New config fields:
client.webhook_config.delivery_history_retention_daysclientblockwebhooks[].max_retrieswebhooksarraywebhooks[].attempt_timeout_secondswebhooksarraywebhooks[].max_concurrent_deliverieswebhooksarrayBreaking changes
Security considerations
Authorization) are encrypted at rest and redacted in all API responses; only header names are visible.webhook-id,webhook-timestamp,webhook-signature,content-type) cannot be overridden by callers.allow_private_network: true; link-local and metadata addresses are always blocked regardless of that flag.Checklist
docs/contributing/README.mdand followed the guidelines