package providers import ( "fmt" "github.com/tidwall/gjson" "gopkg.in/yaml.v3" ) // Provider represents a single API key provider definition loaded from YAML. type Provider struct { FormatVersion int `yaml:"format_version"` Name string `yaml:"name"` DisplayName string `yaml:"display_name"` Tier int `yaml:"tier"` LastVerified string `yaml:"last_verified"` Keywords []string `yaml:"keywords"` Patterns []Pattern `yaml:"patterns"` Verify VerifySpec `yaml:"verify"` } // Pattern defines a single regex pattern for API key detection. type Pattern struct { Regex string `yaml:"regex"` EntropyMin float64 `yaml:"entropy_min"` Confidence string `yaml:"confidence"` } // VerifySpec defines how to verify a key is live (used by Phase 5 verification engine). type VerifySpec struct { Method string `yaml:"method"` URL string `yaml:"url"` Headers map[string]string `yaml:"headers"` // Body is an optional request body template; supports {{KEY}} substitution. Body string `yaml:"body"` // Canonical status code fields (Phase 5) SuccessCodes []int `yaml:"success_codes"` FailureCodes []int `yaml:"failure_codes"` RateLimitCodes []int `yaml:"rate_limit_codes"` // MetadataPaths maps display-name -> gjson path (e.g. "org" -> "organization.name"). MetadataPaths map[string]string `yaml:"metadata_paths"` // Legacy fields kept for backward compat with existing YAMLs (Phase 2-3 providers). ValidStatus []int `yaml:"valid_status"` InvalidStatus []int `yaml:"invalid_status"` } // EffectiveSuccessCodes returns SuccessCodes if non-empty, else falls back to // legacy ValidStatus, else the default [200]. func (v VerifySpec) EffectiveSuccessCodes() []int { if len(v.SuccessCodes) > 0 { return v.SuccessCodes } if len(v.ValidStatus) > 0 { return v.ValidStatus } return []int{200} } // EffectiveFailureCodes returns FailureCodes if non-empty, else falls back to // legacy InvalidStatus, else the default [401, 403]. func (v VerifySpec) EffectiveFailureCodes() []int { if len(v.FailureCodes) > 0 { return v.FailureCodes } if len(v.InvalidStatus) > 0 { return v.InvalidStatus } return []int{401, 403} } // EffectiveRateLimitCodes returns RateLimitCodes if non-empty, else the default [429]. func (v VerifySpec) EffectiveRateLimitCodes() []int { if len(v.RateLimitCodes) > 0 { return v.RateLimitCodes } return []int{429} } // ExtractMetadata applies MetadataPaths (gjson expressions) to a JSON response // body and returns a display-name -> value map. Paths that do not resolve are // skipped. Returns nil if no paths are configured or the body is empty. // Plan 05-03 may extend this with type coercion and nested extraction. func (v VerifySpec) ExtractMetadata(jsonBody []byte) map[string]string { if len(v.MetadataPaths) == 0 || len(jsonBody) == 0 { return nil } out := make(map[string]string, len(v.MetadataPaths)) for name, path := range v.MetadataPaths { result := gjson.GetBytes(jsonBody, path) if result.Exists() { out[name] = result.String() } } if len(out) == 0 { return nil } return out } // RegistryStats holds aggregate statistics about loaded providers. type RegistryStats struct { Total int ByTier map[int]int ByConfidence map[string]int } // UnmarshalYAML implements yaml.Unmarshaler with schema validation (satisfies PROV-10). func (p *Provider) UnmarshalYAML(value *yaml.Node) error { // Use a type alias to avoid infinite recursion type ProviderAlias Provider var alias ProviderAlias if err := value.Decode(&alias); err != nil { return err } if alias.FormatVersion < 1 { return fmt.Errorf("provider %q: format_version must be >= 1 (got %d)", alias.Name, alias.FormatVersion) } if alias.LastVerified == "" { return fmt.Errorf("provider %q: last_verified is required", alias.Name) } validConfidences := map[string]bool{"high": true, "medium": true, "low": true, "": true} for _, pat := range alias.Patterns { if !validConfidences[pat.Confidence] { return fmt.Errorf("provider %q: pattern confidence %q must be high, medium, or low", alias.Name, pat.Confidence) } } *p = Provider(alias) return nil }