2026-09-06 21:40:15 +00:00
package configkit
import (
2026-09-07 01:08:43 +00:00
"cmp"
2026-09-06 21:40:15 +00:00
"encoding/json"
"fmt"
"io"
"reflect"
"slices"
"strconv"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
// Secret is an opaque reference to an environment variable. Its name is public
// configuration; its resolved value is available only through Value.
type Secret struct {
name string
value string
specified bool
resolved bool
}
// SecretEnv constructs an unresolved environment-backed secret reference.
// Invalid names are reported if the reference survives a configuration load.
func SecretEnv ( name string ) Secret {
return Secret { name : name , specified : true }
}
// EnvName returns the public environment-variable name.
func ( s Secret ) EnvName ( ) string {
return s . name
}
// Value returns the resolved credential, or an empty string when unresolved.
func ( s Secret ) Value ( ) string {
if ! s . resolved {
return ""
}
return s . value
}
// IsResolved reports whether this reference resolved to a nonempty value during
// its configuration load.
func ( s Secret ) IsResolved ( ) bool {
return s . resolved
}
// Format renders only the public reference name for every formatting verb.
func ( s Secret ) Format ( state fmt . State , _ rune ) {
_ , _ = io . WriteString ( state , "configkit.SecretEnv(" + strconv . Quote ( s . name ) + ")" )
}
// MarshalJSON emits only the public environment-variable name.
func ( s Secret ) MarshalJSON ( ) ( [ ] byte , error ) {
return json . Marshal ( s . name )
}
type secretLookup func ( string ) ( string , bool )
type secretLookupResult struct {
value string
present bool
}
type secretResolutionResult struct {
lookups map [ string ] secretLookupResult
2026-09-06 22:14:54 +00:00
references map [ string ] string
2026-09-06 21:40:15 +00:00
diagnostics [ ] Diagnostic
}
func resolveSecretReferences ( root reflect . Value , schema * schemaDescriptor , input * document . Node , lookup secretLookup ) secretResolutionResult {
result := secretResolutionResult {
2026-09-06 22:14:54 +00:00
lookups : make ( map [ string ] secretLookupResult ) , references : make ( map [ string ] string ) ,
2026-09-06 21:40:15 +00:00
}
if schema == nil || schema . root == nil || ! root . IsValid ( ) || root . Type ( ) != schema . root . typeOf {
result . diagnostics = [ ] Diagnostic { { Code : CodeSecretRequired , Severity : SeverityError , Message : "configuration value does not match its secret-reference schema" } }
return result
}
if lookup == nil {
lookup = func ( string ) ( string , bool ) { return "" , false }
}
resolveSecretValue ( root , schema . root , Path { } , false , input , lookup , & result )
result . diagnostics = canonicalDiagnostics ( result . diagnostics )
return result
}
func resolveSecretValue ( value reflect . Value , descriptor * typeDescriptor , path Path , optional bool , input * document . Node , lookup secretLookup , result * secretResolutionResult ) {
if descriptor . kind == descriptorPointer {
if value . IsNil ( ) {
return
}
resolveSecretValue ( value . Elem ( ) , descriptor . element , path , descriptor . element . kind == descriptorSecret , input , lookup , result )
return
}
switch descriptor . kind {
case descriptorSecret :
reference := value . Interface ( ) . ( Secret )
name := reference . EnvName ( )
if name == "" {
code := CodeSecretRequired
message := "required secret reference is absent"
if optional || reference . specified {
code = CodeSecretInvalidName
message = "secret environment name is empty"
}
result . diagnostics = append ( result . diagnostics , secretDiagnostic ( code , path , message , input ) )
return
}
if ! validEnvironmentName ( name ) {
result . diagnostics = append ( result . diagnostics , secretDiagnostic ( CodeSecretInvalidName , path , "secret environment name is invalid" , input ) )
return
}
found , exists := result . lookups [ name ]
if ! exists {
found . value , found . present = lookup ( name )
result . lookups [ name ] = found
}
if ! found . present {
result . diagnostics = append ( result . diagnostics , secretDiagnostic ( CodeSecretUnset , path , "secret environment variable is not set" , input ) )
return
}
if found . value == "" {
result . diagnostics = append ( result . diagnostics , secretDiagnostic ( CodeSecretEmpty , path , "secret environment variable is empty" , input ) )
return
}
value . Set ( reflect . ValueOf ( Secret { name : name , value : found . value , specified : true , resolved : true } ) )
2026-09-06 22:14:54 +00:00
result . references [ path . String ( ) ] = name
2026-09-06 21:40:15 +00:00
case descriptorStruct :
for _ , member := range descriptor . members {
memberPath := path
if ! member . inline {
memberPath = path . Key ( member . name )
}
resolveSecretValue ( value . Field ( member . index ) , member . descriptor , memberPath , false , input , lookup , result )
}
case descriptorMap :
if value . IsNil ( ) {
return
}
keys := value . MapKeys ( )
2026-09-07 01:08:43 +00:00
slices . SortFunc ( keys , func ( left , right reflect . Value ) int { return cmp . Compare ( left . String ( ) , right . String ( ) ) } )
2026-09-06 21:40:15 +00:00
for _ , key := range keys {
entry := reflect . New ( value . Type ( ) . Elem ( ) ) . Elem ( )
entry . Set ( value . MapIndex ( key ) )
resolveSecretValue ( entry , descriptor . element , path . Key ( key . String ( ) ) , false , input , lookup , result )
value . SetMapIndex ( key , entry )
}
case descriptorSlice , descriptorArray :
if descriptor . kind == descriptorSlice && value . IsNil ( ) {
return
}
for index := 0 ; index < value . Len ( ) ; index ++ {
resolveSecretValue ( value . Index ( index ) , descriptor . element , path . Index ( index ) , false , input , lookup , result )
}
}
}
func secretDiagnostic ( code Code , path Path , message string , input * document . Node ) Diagnostic {
diagnostic := Diagnostic { Code : code , Severity : SeverityError , Path : path . clone ( ) , Message : message }
if location , ok := nearestDocumentLocation ( input , path ) ; ok {
diagnostic . Primary = documentLocationPointer ( location )
}
return diagnostic
}
func changedSecretStates ( root reflect . Value , descriptor * typeDescriptor , lookups map [ string ] secretLookupResult ) [ ] Path {
var changed [ ] Path
collectChangedSecretStates ( root , descriptor , Path { } , lookups , & changed )
2026-09-07 01:08:43 +00:00
slices . SortFunc ( changed , func ( left , right Path ) int { return cmp . Compare ( left . String ( ) , right . String ( ) ) } )
2026-09-06 21:40:15 +00:00
return changed
}
func collectChangedSecretStates ( value reflect . Value , descriptor * typeDescriptor , path Path , lookups map [ string ] secretLookupResult , changed * [ ] Path ) {
if descriptor . kind == descriptorPointer {
if ! value . IsNil ( ) {
collectChangedSecretStates ( value . Elem ( ) , descriptor . element , path , lookups , changed )
}
return
}
switch descriptor . kind {
case descriptorSecret :
reference := value . Interface ( ) . ( Secret )
expected , ok := lookups [ reference . EnvName ( ) ]
if ok && ( ! expected . present || expected . value == "" || ! reference . resolved || reference . value != expected . value ) {
* changed = append ( * changed , path . clone ( ) )
}
case descriptorStruct :
for _ , member := range descriptor . members {
memberPath := path
if ! member . inline {
memberPath = path . Key ( member . name )
}
collectChangedSecretStates ( value . Field ( member . index ) , member . descriptor , memberPath , lookups , changed )
}
case descriptorMap :
if value . IsNil ( ) {
return
}
keys := value . MapKeys ( )
2026-09-07 01:08:43 +00:00
slices . SortFunc ( keys , func ( left , right reflect . Value ) int { return cmp . Compare ( left . String ( ) , right . String ( ) ) } )
2026-09-06 21:40:15 +00:00
for _ , key := range keys {
collectChangedSecretStates ( value . MapIndex ( key ) , descriptor . element , path . Key ( key . String ( ) ) , lookups , changed )
}
case descriptorSlice , descriptorArray :
if descriptor . kind == descriptorSlice && value . IsNil ( ) {
return
}
for index := 0 ; index < value . Len ( ) ; index ++ {
collectChangedSecretStates ( value . Index ( index ) , descriptor . element , path . Index ( index ) , lookups , changed )
}
}
}
2026-09-07 01:14:17 +00:00
func cloneSecretReferences ( values map [ string ] string ) map [ string ] string {
if values == nil {
return nil
}
cloned := make ( map [ string ] string , len ( values ) )
for path , name := range values {
cloned [ path ] = name
}
return cloned
}