Webhook-to-SSE gateway with hierarchical topic routing and signature verification

Support environment variable interpolation in config files

Runs os.ExpandEnv on the raw YAML before unmarshaling, so secrets can
come from environment variables (e.g. ${WEBHOOK_SECRET}) rather than
being hardcoded. Handy for k8s where the config lives in a ConfigMap
but secrets come from Secret objects mapped to env vars.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+42 -1
+2 -1
configuration.go
··· 25 25 if err != nil { 26 26 return nil, err 27 27 } 28 + expanded := os.ExpandEnv(string(data)) 28 29 var cfg Configuration 29 - if err := yaml.Unmarshal(data, &cfg); err != nil { 30 + if err := yaml.Unmarshal([]byte(expanded), &cfg); err != nil { 30 31 return nil, err 31 32 } 32 33 return &cfg, nil
+40
configuration_test.go
··· 217 217 } 218 218 } 219 219 220 + func TestLoadConfiguration_expandsEnvVars(t *testing.T) { 221 + t.Setenv("WICKET_TEST_SECRET", "expanded-secret") 222 + dir := t.TempDir() 223 + path := filepath.Join(dir, "wicket.yaml") 224 + os.WriteFile(path, []byte(` 225 + paths: 226 + test/path: 227 + verify: hmac-sha256 228 + secret: "${WICKET_TEST_SECRET}" 229 + `), 0644) 230 + 231 + cfg, err := LoadConfiguration(path) 232 + if err != nil { 233 + t.Fatalf("unexpected error: %v", err) 234 + } 235 + pc := cfg.Paths["test/path"] 236 + if pc.Secret != "expanded-secret" { 237 + t.Errorf("expected expanded-secret, got %s", pc.Secret) 238 + } 239 + } 240 + 241 + func TestLoadConfiguration_unexpandedEnvVar(t *testing.T) { 242 + dir := t.TempDir() 243 + path := filepath.Join(dir, "wicket.yaml") 244 + os.WriteFile(path, []byte(` 245 + paths: 246 + test/path: 247 + secret: "${WICKET_UNSET_VAR_xyz}" 248 + `), 0644) 249 + 250 + cfg, err := LoadConfiguration(path) 251 + if err != nil { 252 + t.Fatalf("unexpected error: %v", err) 253 + } 254 + pc := cfg.Paths["test/path"] 255 + if pc.Secret != "" { 256 + t.Errorf("expected empty string for unset var, got %s", pc.Secret) 257 + } 258 + } 259 + 220 260 func TestLookupSubscribeSecret_nilConfig(t *testing.T) { 221 261 var cfg *Configuration 222 262 secret := cfg.LookupSubscribeSecret("any/path")