mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-10 15:05:40 +00:00
bfa32dde5a
Webhook signing secrets were stored plaintext in the webhooks.secret column and echoed back in every API response. Encrypt them at rest (reusing the existing AES-256-GCM store helpers, same pattern as TOTP secrets) and return the raw secret ONLY in the creation response; list responses now mask it and expose a has_secret flag instead. - store: encrypt on CreateWebhook, decrypt on Get/ListWebhooks so the dispatcher still signs with the plaintext secret. Reuses the secret column with the "enc:" prefix — no new column/migration. Keyless self-host stays a no-op fallback (encrypt returns plaintext; decrypt passes legacy rows through unchanged). - BackfillEncryptWebhookSecrets encrypts pre-existing plaintext rows on startup once a key is configured (idempotent), mirroring the TOTP backfill. - model: add HasSecret so masked responses still signal presence. - handlers: mask secret on list; document raw-only-on-create. - tests: encrypt-at-rest round-trip + HMAC validity, list decrypt, plaintext backfill/back-compat, and the API mask-except-on-create contract. Claude-Session: https://claude.ai/code/session_015yuBJQYfDj95cgX3DaD8SF
34 lines
1.4 KiB
Go
34 lines
1.4 KiB
Go
package models
|
|
|
|
import "time"
|
|
|
|
// Webhook represents a registered webhook endpoint that receives
|
|
// POST notifications when events occur in a workspace.
|
|
type Webhook struct {
|
|
ID string `json:"id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
URL string `json:"url"`
|
|
// Secret is the HMAC signing secret in PLAINTEXT. It is encrypted at rest
|
|
// (see internal/store/webhooks.go) and decrypted on read for internal use
|
|
// (the dispatcher signs payloads with it). API responses return the raw
|
|
// secret ONLY on creation; list/get responses mask it — see
|
|
// internal/server/handlers_webhooks.go::maskWebhookSecret.
|
|
Secret string `json:"secret,omitempty"`
|
|
// HasSecret reports whether an HMAC secret is configured, without leaking
|
|
// the value. Populated on reads so masked responses still signal presence.
|
|
HasSecret bool `json:"has_secret"`
|
|
Events string `json:"events"`
|
|
Active bool `json:"active"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
LastTriggeredAt *time.Time `json:"last_triggered_at,omitempty"`
|
|
FailureCount int `json:"failure_count"`
|
|
}
|
|
|
|
// WebhookCreate is the input for registering a new webhook.
|
|
type WebhookCreate struct {
|
|
URL string `json:"url"`
|
|
Secret string `json:"secret,omitempty"`
|
|
Events string `json:"events,omitempty"` // JSON array of event types, defaults to ["*"]
|
|
}
|