1
0
forked from wrenn/wrenn

feat: add notification channels with provider integrations and retry

Implement a channels system for notifying teams via external providers
(Discord, Slack, Teams, Google Chat, Telegram, Matrix, webhook) when
lifecycle events occur (capsule/template/host state changes).

- Channel CRUD API under /v1/channels (JWT-only auth)
- Test endpoint to verify config before saving (POST /v1/channels/test)
- Secret rotation endpoint (PUT /v1/channels/{id}/config)
- AES-256-GCM encryption for provider secrets (WRENN_ENCRYPTION_KEY)
- Redis stream event publishing from audit logger
- Background dispatcher with consumer group and retry (10s, 30s)
- Webhook delivery with HMAC-SHA256 signing (X-WRENN-SIGNATURE)
- shoutrrr integration for chat providers
- Secrets never exposed in API responses
This commit is contained in:
2026-04-09 17:06:06 +06:00
parent 5148b5dd64
commit 84dd15d22b
24 changed files with 1871 additions and 7 deletions

View File

@ -1,6 +1,7 @@
package config
import (
"encoding/hex"
"os"
"github.com/joho/godotenv"
@ -22,6 +23,10 @@ type Config struct {
OAuthGitHubClientSecret string
OAuthRedirectURL string
CPPublicURL string
// Channels — encryption for channel secrets (AES-256-GCM).
EncryptionKeyHex string // WRENN_ENCRYPTION_KEY raw hex string (for validation)
EncryptionKey [32]byte // parsed 32-byte key
}
// Load reads configuration from a .env file (if present) and environment variables.
@ -30,7 +35,7 @@ func Load() Config {
// Best-effort load — missing .env file is fine.
_ = godotenv.Load()
return Config{
cfg := Config{
DatabaseURL: envOrDefault("DATABASE_URL", "postgres://wrenn:wrenn@localhost:5432/wrenn?sslmode=disable"),
RedisURL: envOrDefault("REDIS_URL", "redis://localhost:6379/0"),
ListenAddr: envOrDefault("WRENN_CP_LISTEN_ADDR", ":8080"),
@ -43,7 +48,18 @@ func Load() Config {
OAuthGitHubClientSecret: os.Getenv("OAUTH_GITHUB_CLIENT_SECRET"),
OAuthRedirectURL: envOrDefault("OAUTH_REDIRECT_URL", "https://app.wrenn.dev"),
CPPublicURL: os.Getenv("CP_PUBLIC_URL"),
EncryptionKeyHex: os.Getenv("WRENN_ENCRYPTION_KEY"),
}
if cfg.EncryptionKeyHex != "" {
b, err := hex.DecodeString(cfg.EncryptionKeyHex)
if err == nil && len(b) == 32 {
copy(cfg.EncryptionKey[:], b)
}
}
return cfg
}
func envOrDefault(key, def string) string {