package config import "strings" // ConfigKey declares one configuration key an integration owns — see // design.md's "Declared config, spec-driven validation" decision // (openspec/changes/integration-extraction/design.md). It is the single // declaration cmd/start.go, this package's ValidateStart, and (once task // 4.4 lands) internal/embeds/mc-config.yaml all derive their behavior // from, replacing three previously hand-maintained, independently-drifting // lists (cmd's flag/viper-default calls, validate.go's hardcoded // per-integration conditional, and the YAML defaults file). // // Name is the viper/flag key (kebab-case, e.g. "stripe-api-key"). Default, // when non-nil, is bound via viper.SetDefault; its dynamic type (string or // []string today) tells cmd which cobra flag constructor to use. Usage is // the flag's help text. Secret marks the key as sensitive: cmd also binds // a "-file" flag and, before validation runs, resolves a configured // file path into Name (loading the value from disk) — mirroring the // handling core's own secrets (csrf-secret, oidc-sp-client-secret, ...) // already receive. RequiredGroup, when non-empty, ties this key to every // other declared key sharing the same tag: ValidateStart rejects a // configuration where some but not all of a group's keys are set (e.g. // Stripe's stripe-api-key and stripe-webhook-secret share the tag // "Stripe" — both must be set to enable billing, or both left empty to // disable it). type ConfigKey struct { Name string Default any Usage string Secret bool RequiredGroup string // Enum, when non-empty, closes the key's value set. The runtime // settings surface renders such keys as a select and rejects // non-members at save time; the boot overlay re-checks stored // overrides. ValidateStart also checks every declared Enum key's // resolved environment/flag/config-file value for membership (design // D8, schema-hardening), sharing enumMember with CoerceOverride so the // two paths cannot diverge. Enum []string } // ConfigProvider is implemented by integrations that declare their own // configuration keys, so the composition root (cmd/start.go) can bind // flags/env/defaults and this package can validate presence and // required-together groups without a hardcoded per-integration block. It // is declared here — in internal/config, not internal/integrations — // because this package is the one that actually consumes ConfigKey values // (ValidateStart's required-group check) and cmd/start.go already imports // it for that call; declaring ConfigKey/ConfigProvider here instead means // internal/config never has to import internal/integrations (and, // transitively, every concrete adapter's own dependencies — internal/ // server, internal/workflows/*, the Temporal client, net/http) merely to // reference a plain declaration struct. This mirrors why server. // RouteProvider and workflows.WorkflowProvider are declared in their own // consuming packages rather than in internal/integrations. type ConfigProvider interface { ConfigSpec() []ConfigKey } // SecretPair names a secret configuration key and its "-file" // companion flag, as cmd binds them. SecretPairsFrom derives the list cmd // resolves (load the file, if any, into Name) before validation runs, so // that list is data-driven from declarations instead of a hand-maintained // literal per integration. type SecretPair struct { Name string FileName string } // SecretPairsFrom returns the {Name, "-file"} pair for every // declared key with Secret set, in declaration order. func SecretPairsFrom(keys []ConfigKey) []SecretPair { var pairs []SecretPair for _, k := range keys { if k.Secret { pairs = append(pairs, SecretPair{Name: k.Name, FileName: k.Name + "-file"}) } } return pairs } // RequiredKeysUnresolved returns the names, in declaration order, of every // key whose RequiredGroup is non-empty (ValidateStart's "required // together" keys — see validateRequiredGroups) and whose resolved value // (environment/flag/config-file/default, read straight from viper — the // same layer ValidateStart and the settings page itself read) is empty // after trimming. A key with no RequiredGroup imposes no requirement and // is never reported. // // This is the ONE resolution every honest-status surface shares (ux- // honest-surfaces design decision 2): the Integrations list, the // overview's system region, and the per-integration settings page all // derive their configuration-readiness signal from this function, so // "not configured" means the same thing everywhere it appears. Secret // keys are covered too — they are still bound into viper by cmd/start.go // (via the "-file" resolution), only their *value* is withheld from // display, never their presence. func RequiredKeysUnresolved(keys []ConfigKey) []string { var missing []string for _, k := range keys { if k.RequiredGroup == "" { continue } // effectiveString (overlay.go) is the same per-key resolution // ApplyOverlay and validateRequiredGroups already read; trimming // matches validateRequiredGroups' own presence check exactly. if strings.TrimSpace(effectiveString(k)) == "" { missing = append(missing, k.Name) } } return missing }