2026-09-12 15:58:06 +00:00
package engine
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
)
const (
secretsEntryLimit = 1024
secretFileByteLimit = int64 ( 1 << 20 )
secretsByteLimit = int64 ( 8 << 20 )
)
// SecretFileStatus describes the outcome for one file considered by
// LoadSecretsDir.
type SecretFileStatus string
const (
SecretFileLoaded SecretFileStatus = "loaded"
SecretFileSkippedExisting SecretFileStatus = "skipped_existing"
SecretFileNotApplied SecretFileStatus = "not_applied"
SecretFileFailed SecretFileStatus = "failed"
)
// SecretFileResult is the safe, value-free bootstrap result for one directory
// entry. ExistingEmpty is meaningful when Status is SecretFileSkippedExisting.
type SecretFileResult struct {
Name string
Path string
Status SecretFileStatus
ExistingEmpty bool
}
// SecretsReport is a detached snapshot of an explicit secrets-directory
// bootstrap. It does not retain file contents or environment values.
type SecretsReport struct {
entries [ ] SecretFileResult
}
// Entries returns a copy of the report's filename-ordered entries.
func ( r * SecretsReport ) Entries ( ) [ ] SecretFileResult {
if r == nil {
return nil
}
return slices . Clone ( r . entries )
}
// WriteTo renders one safe, deterministic line per entry. Names and paths are
// quoted so control characters cannot affect terminal output.
func ( r * SecretsReport ) WriteTo ( output io . Writer ) ( int64 , error ) {
if output == nil {
return 0 , errors . New ( "configkit: secrets report output is nil" )
}
if r == nil {
return 0 , nil
}
var total int64
for _ , entry := range r . entries {
line := "status=" + strconv . Quote ( string ( entry . Status ) ) +
" name=" + strconv . Quote ( entry . Name ) +
" path=" + strconv . Quote ( entry . Path ) +
" existing_empty=" + strconv . FormatBool ( entry . ExistingEmpty ) + "\n"
written , err := io . WriteString ( output , line )
total += int64 ( written )
if err != nil {
return total , err
}
if written != len ( line ) {
return total , io . ErrShortWrite
}
}
return total , nil
}
// SecretsError reports safe structured diagnostics for a failed explicit
// secrets-directory bootstrap. It intentionally does not unwrap filesystem or
// environment errors.
type SecretsError struct {
diagnostics [ ] Diagnostic
}
// Error implements error without rendering filenames, paths, or file contents.
func ( e * SecretsError ) Error ( ) string {
if e == nil {
return "configkit: secrets bootstrap failed"
}
count := 0
for _ , diagnostic := range e . diagnostics {
if diagnostic . Severity == SeverityError {
count ++
}
}
if count == 1 {
return "configkit: secrets bootstrap failed with 1 error"
}
return fmt . Sprintf ( "configkit: secrets bootstrap failed with %d errors" , count )
}
// Diagnostics returns a deep copy of the bootstrap diagnostics.
func ( e * SecretsError ) Diagnostics ( ) [ ] Diagnostic {
if e == nil {
return nil
}
return cloneDiagnostics ( e . diagnostics )
}
type secretsEnvironment struct {
lookup func ( string ) ( string , bool )
set func ( string , string ) error
}
type secretsAccess struct {
openDirectory func ( string ) ( * os . File , error )
lstat func ( string ) ( fs . FileInfo , error )
openFile func ( string ) ( * os . File , error )
}
type stagedSecret struct {
entry int
value string
}
// LoadSecretsDir explicitly loads immediate regular files from directory into
// previously unset process environment variables. Applications should call it
// once during startup, before concurrent work; Load never calls it.
2026-09-21 13:26:01 +00:00
// Dot-prefixed entries are ignored and omitted from the report.
2026-09-12 15:58:06 +00:00
func LoadSecretsDir ( directory string ) ( * SecretsReport , error ) {
return loadSecretsDir ( directory , secretsEnvironment { lookup : os . LookupEnv , set : os . Setenv } , secretsAccess {
openDirectory : os . Open ,
lstat : os . Lstat ,
openFile : os . Open ,
} )
}
func loadSecretsDir ( directory string , environment secretsEnvironment , access secretsAccess ) ( * SecretsReport , error ) {
report := & SecretsReport { }
if strings . TrimSpace ( directory ) == "" {
return report , secretsFailure ( CodeSecretsInvalidPath , "secrets directory path is required" )
}
canonical , err := canonicalExistingPath ( directory )
if err != nil {
return report , secretsFailure ( classifySecretsFilesystemError ( err ) , "secrets directory could not be canonicalized: " + strconv . Quote ( cleanAbsolutePath ( directory ) ) )
}
info , err := os . Stat ( canonical )
if err != nil {
return report , secretsFailure ( classifySecretsFilesystemError ( err ) , "secrets directory could not be inspected: " + strconv . Quote ( canonical ) )
}
if ! info . IsDir ( ) {
return report , secretsFailure ( CodeSecretsInvalidPath , "secrets directory is not a directory: " + strconv . Quote ( canonical ) )
}
directoryFile , err := access . openDirectory ( canonical )
if err != nil {
return report , secretsFailure ( classifySecretsFilesystemError ( err ) , "secrets directory could not be opened: " + strconv . Quote ( canonical ) )
}
entries , readErr := readSecretsEntries ( directoryFile )
closeErr := directoryFile . Close ( )
if readErr != nil {
return report , secretsFailure ( classifySecretsFilesystemError ( readErr ) , "secrets directory could not be enumerated: " + strconv . Quote ( canonical ) )
}
if closeErr != nil {
return report , secretsFailure ( CodeSecretsRead , "secrets directory could not be closed: " + strconv . Quote ( canonical ) )
}
2026-09-21 13:26:01 +00:00
entryLimitExceeded := len ( entries ) > secretsEntryLimit
entries = slices . DeleteFunc ( entries , func ( entry fs . DirEntry ) bool {
return strings . HasPrefix ( entry . Name ( ) , "." )
} )
2026-09-12 15:58:06 +00:00
slices . SortFunc ( entries , func ( left , right fs . DirEntry ) int { return strings . Compare ( left . Name ( ) , right . Name ( ) ) } )
report . entries = make ( [ ] SecretFileResult , len ( entries ) )
for index , entry := range entries {
name := entry . Name ( )
report . entries [ index ] = SecretFileResult { Name : name , Path : filepath . Join ( canonical , name ) , Status : SecretFileNotApplied }
}
2026-09-21 13:26:01 +00:00
if entryLimitExceeded {
2026-09-12 15:58:06 +00:00
return report , secretsFailure ( CodeSecretsLimitExceeded , "secrets directory exceeds the 1024-entry limit: " + strconv . Quote ( canonical ) )
}
for index := range report . entries {
entry := & report . entries [ index ]
if ! validEnvironmentName ( entry . Name ) {
entry . Status = SecretFileFailed
return report , secretsFailure ( CodeSecretsInvalidName , "secret filename is not a valid environment name: " + strconv . Quote ( entry . Path ) )
}
info , err := access . lstat ( entry . Path )
if err != nil {
entry . Status = SecretFileFailed
return report , secretsFailure ( classifySecretsFilesystemError ( err ) , "secret file could not be inspected: " + strconv . Quote ( entry . Path ) )
}
if info . Mode ( ) & fs . ModeSymlink != 0 || ! info . Mode ( ) . IsRegular ( ) {
entry . Status = SecretFileFailed
return report , secretsFailure ( CodeSecretsNotRegular , "secret entry is not a regular file: " + strconv . Quote ( entry . Path ) )
}
}
staged := make ( [ ] stagedSecret , 0 , len ( report . entries ) )
var consumed int64
for index := range report . entries {
entry := & report . entries [ index ]
if existing , present := environment . lookup ( entry . Name ) ; present {
entry . Status = SecretFileSkippedExisting
entry . ExistingEmpty = existing == ""
continue
}
remaining := secretsByteLimit - consumed
value , used , err := readSecretValue ( entry . Path , remaining , access )
if err != nil {
entry . Status = SecretFileFailed
return report , secretsFailure ( secretReadCode ( err ) , secretReadMessage ( err ) + ": " + strconv . Quote ( entry . Path ) )
}
consumed += used
staged = append ( staged , stagedSecret { entry : index , value : value } )
}
for _ , secret := range staged {
entry := & report . entries [ secret . entry ]
if existing , present := environment . lookup ( entry . Name ) ; present {
entry . Status = SecretFileSkippedExisting
entry . ExistingEmpty = existing == ""
continue
}
if err := environment . set ( entry . Name , secret . value ) ; err != nil {
entry . Status = SecretFileFailed
return report , secretsFailure ( CodeSecretsApplyFailed , "secret environment variable could not be set for file: " + strconv . Quote ( entry . Path ) )
}
entry . Status = SecretFileLoaded
}
return report , nil
}
func readSecretsEntries ( directory * os . File ) ( [ ] fs . DirEntry , error ) {
entries := make ( [ ] fs . DirEntry , 0 )
for len ( entries ) <= secretsEntryLimit {
batch , err := directory . ReadDir ( secretsEntryLimit + 1 - len ( entries ) )
entries = append ( entries , batch ... )
if err == io . EOF {
return entries , nil
}
if err != nil {
return entries , err
}
if len ( entries ) > secretsEntryLimit {
return entries , nil
}
}
return entries , nil
}
type secretReadFailure int
const (
secretReadFilesystem secretReadFailure = iota
secretReadFileLimit
secretReadTotalLimit
secretReadNUL
)
type secretReadError struct {
kind secretReadFailure
err error
}
func ( e * secretReadError ) Error ( ) string { return "secret read failed" }
func readSecretValue ( path string , remaining int64 , access secretsAccess ) ( string , int64 , error ) {
if remaining < 0 {
return "" , 0 , & secretReadError { kind : secretReadTotalLimit }
}
limit := min ( secretFileByteLimit , remaining )
file , err := access . openFile ( path )
if err != nil {
return "" , 0 , & secretReadError { kind : secretReadFilesystem , err : err }
}
data , readErr := io . ReadAll ( io . LimitReader ( file , limit + 1 ) )
closeErr := file . Close ( )
if readErr != nil {
return "" , 0 , & secretReadError { kind : secretReadFilesystem , err : readErr }
}
if closeErr != nil {
return "" , 0 , & secretReadError { kind : secretReadFilesystem , err : closeErr }
}
if int64 ( len ( data ) ) > limit {
kind := secretReadFileLimit
if remaining < secretFileByteLimit {
kind = secretReadTotalLimit
}
return "" , 0 , & secretReadError { kind : kind }
}
if slices . Contains ( data , byte ( 0 ) ) {
return "" , 0 , & secretReadError { kind : secretReadNUL }
}
value := string ( data )
if strings . HasSuffix ( value , "\r\n" ) {
value = strings . TrimSuffix ( value , "\r\n" )
} else {
value = strings . TrimSuffix ( value , "\n" )
}
return value , int64 ( len ( data ) ) , nil
}
func validEnvironmentName ( name string ) bool {
if name == "" || ! asciiEnvironmentFirst ( name [ 0 ] ) {
return false
}
for index := 1 ; index < len ( name ) ; index ++ {
character := name [ index ]
if ! asciiEnvironmentFirst ( character ) && ( character < '0' || character > '9' ) {
return false
}
}
return true
}
func asciiEnvironmentFirst ( character byte ) bool {
return character == '_' || character >= 'A' && character <= 'Z' || character >= 'a' && character <= 'z'
}
func cleanAbsolutePath ( path string ) string {
absolute , err := filepath . Abs ( path )
if err != nil {
return filepath . Clean ( path )
}
return filepath . Clean ( absolute )
}
func classifySecretsFilesystemError ( err error ) Code {
switch {
case errors . Is ( err , fs . ErrNotExist ) :
return CodeSecretsMissing
case errors . Is ( err , fs . ErrPermission ) :
return CodeSecretsPermission
default :
return CodeSecretsRead
}
}
func secretReadCode ( err error ) Code {
var failure * secretReadError
if errors . As ( err , & failure ) {
switch failure . kind {
case secretReadFileLimit , secretReadTotalLimit :
return CodeSecretsLimitExceeded
case secretReadNUL :
return CodeSecretsInvalidValue
case secretReadFilesystem :
return classifySecretsFilesystemError ( failure . err )
}
}
return CodeSecretsRead
}
func secretReadMessage ( err error ) string {
var failure * secretReadError
if errors . As ( err , & failure ) {
switch failure . kind {
case secretReadFileLimit :
return "secret file exceeds the 1 MiB limit"
case secretReadTotalLimit :
return "eligible secret files exceed the 8 MiB total limit"
case secretReadNUL :
return "secret file contains a NUL byte"
}
}
return "secret file could not be read"
}
func secretsFailure ( code Code , message string ) * SecretsError {
return & SecretsError { diagnostics : [ ] Diagnostic { { Code : code , Severity : SeverityError , Message : message } } }
}