feat: outbound webhooks — signed event delivery + delivery log

Every audited action now emits a "resource.action" event (bare actions
get resource-qualified; already-qualified actions like service.clone
pass through). Webhooks subscribe per-user with exact names, resource.*
wildcards, or *.

- schema: outbound_webhooks + webhook_deliveries (sqlc + goose)
- dispatcher: 4 workers, 512-queue, 3 attempts with backoff, 10s timeout;
  deliveries record status/http-code/body(4KiB)/duration
- signing: X-Containr-Signature sha256=<hmac> over raw body;
  X-Containr-Event + X-Containr-Delivery headers; custom headers per hook
- api: CRUD + /deliveries + /test (ping) under authed group
- fix: SetAuditDB never called — LogAudit was a silent no-op at every
  call site; now wired at boot
- coverage: service create/delete audited (previously uninstrumented)
- cli: containr webhooks list|create|update|delete|deliveries|test
- mcp: 6 tools, delete requires confirm
- frontend: /settings/webhooks page — list/create/expand-deliveries/
  test/disable/delete, secret-shown-once banner; nav + account menu
- openapi + regenerated types; CLI.md + docs snapshot; roadmap ticks
This commit is contained in:
Tomas Dvorak committed 2026-10-09 09:44:08 +02:00
1 parent 5c2f2861bd
commit f499678964
23 files changed
+2278 -7

No files matched your search

+249
View File
@@ -2350,6 +2350,189 @@ paths:
schema:
$ref: '#/components/schemas/MessageResponse'
# Outbound Webhooks
/webhooks:
get:
tags:
- Webhooks
summary: List outbound webhooks
security:
- bearerAuth: []
responses:
'200':
description: Webhooks for the caller (secrets masked)
content:
application/json:
schema:
type: object
properties:
webhooks:
type: array
items:
$ref: '#/components/schemas/OutboundWebhook'
post:
tags:
- Webhooks
summary: Create an outbound webhook
description: |
Receives signed POST deliveries for every audited action matching
the event list — "resource.action" names, "resource.*" wildcards,
or "*" for everything. The secret is returned once; deliveries
carry X-Containr-Signature: sha256=<hmac> over the raw body.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- name
- url
- events
properties:
name:
type: string
url:
type: string
secret:
type: string
description: Signing secret (auto-generated when omitted)
events:
type: array
items:
type: string
example: ["service.*", "deployment.fail"]
headers:
type: object
additionalProperties:
type: string
enabled:
type: boolean
default: true
responses:
'201':
description: Webhook created (secret revealed once)
content:
application/json:
schema:
$ref: '#/components/schemas/OutboundWebhook'
'400':
description: Invalid name, URL, or empty event list
/webhooks/{id}:
patch:
tags:
- Webhooks
summary: Update a webhook
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
url:
type: string
secret:
type: string
events:
type: array
items:
type: string
headers:
type: object
additionalProperties:
type: string
enabled:
type: boolean
responses:
'200':
description: Updated webhook
content:
application/json:
schema:
$ref: '#/components/schemas/OutboundWebhook'
'404':
description: Webhook not found
delete:
tags:
- Webhooks
summary: Delete a webhook
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Deleted
content:
application/json:
schema:
$ref: '#/components/schemas/MessageResponse'
/webhooks/{id}/deliveries:
get:
tags:
- Webhooks
summary: List delivery attempts for a webhook
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Recent deliveries (newest first, max 100)
content:
application/json:
schema:
type: object
properties:
deliveries:
type: array
items:
$ref: '#/components/schemas/WebhookDelivery'
/webhooks/{id}/test:
post:
tags:
- Webhooks
summary: Queue a test ping delivery
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'202':
description: Delivery queued
content:
application/json:
schema:
$ref: '#/components/schemas/MessageResponse'
# Audit Logs
/audit-logs:
get:
@@ -6953,6 +7136,72 @@ components:
unread:
type: integer
OutboundWebhook:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
url:
type: string
secret:
type: string
description: Only present on create/update responses
events:
type: array
items:
type: string
headers:
type: object
additionalProperties:
type: string
enabled:
type: boolean
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
WebhookDelivery:
type: object
properties:
id:
type: string
format: uuid
webhook_id:
type: string
format: uuid
event:
type: string
payload:
type: object
additionalProperties: true
status:
type: string
enum: [pending, success, failed]
response_status:
type: integer
nullable: true
response_body:
type: string
nullable: true
attempts:
type: integer
duration_ms:
type: integer
nullable: true
created_at:
type: string
format: date-time
delivered_at:
type: string
format: date-time
nullable: true
AuditLogListResponse:
type: object
properties:
+4
View File
@@ -78,6 +78,10 @@ security scan|vulnerabilities|metrics
gateway services|keys|analytics (admin)
admin overview|users|settings|audit-logs
notifications list|read
webhooks list|create|update|delete|deliveries|test
outbound signed webhooks — create <name> <url>
--event 'service.*' (repeatable, '*' = all);
deliveries are HMAC-signed (X-Containr-Signature)
up deploy the current directory
```