 131868bdca
			
		
	
	131868bdca
	
	
	
		
			
			Major security, observability, and configuration improvements:
## Security Hardening
- Implemented configurable CORS (no more wildcards)
- Added comprehensive auth middleware for admin endpoints
- Enhanced webhook HMAC validation
- Added input validation and rate limiting
- Security headers and CSP policies
## Configuration Management
- Made N8N webhook URL configurable (WHOOSH_N8N_BASE_URL)
- Replaced all hardcoded endpoints with environment variables
- Added feature flags for LLM vs heuristic composition
- Gitea fetch hardening with EAGER_FILTER and FULL_RESCAN options
## API Completeness
- Implemented GetCouncilComposition function
- Added GET /api/v1/councils/{id} endpoint
- Council artifacts API (POST/GET /api/v1/councils/{id}/artifacts)
- /admin/health/details endpoint with component status
- Database lookup for repository URLs (no hardcoded fallbacks)
## Observability & Performance
- Added OpenTelemetry distributed tracing with goal/pulse correlation
- Performance optimization database indexes
- Comprehensive health monitoring
- Enhanced logging and error handling
## Infrastructure
- Production-ready P2P discovery (replaces mock implementation)
- Removed unused Redis configuration
- Enhanced Docker Swarm integration
- Added migration files for performance indexes
## Code Quality
- Comprehensive input validation
- Graceful error handling and failsafe fallbacks
- Backwards compatibility maintained
- Following security best practices
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
		
	
		
			
				
	
	
		
			383 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			383 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) 2013 Kelsey Hightower. All rights reserved.
 | |
| // Use of this source code is governed by the MIT License that can be found in
 | |
| // the LICENSE file.
 | |
| 
 | |
| package envconfig
 | |
| 
 | |
| import (
 | |
| 	"encoding"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"reflect"
 | |
| 	"regexp"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| // ErrInvalidSpecification indicates that a specification is of the wrong type.
 | |
| var ErrInvalidSpecification = errors.New("specification must be a struct pointer")
 | |
| 
 | |
| var gatherRegexp = regexp.MustCompile("([^A-Z]+|[A-Z]+[^A-Z]+|[A-Z]+)")
 | |
| var acronymRegexp = regexp.MustCompile("([A-Z]+)([A-Z][^A-Z]+)")
 | |
| 
 | |
| // A ParseError occurs when an environment variable cannot be converted to
 | |
| // the type required by a struct field during assignment.
 | |
| type ParseError struct {
 | |
| 	KeyName   string
 | |
| 	FieldName string
 | |
| 	TypeName  string
 | |
| 	Value     string
 | |
| 	Err       error
 | |
| }
 | |
| 
 | |
| // Decoder has the same semantics as Setter, but takes higher precedence.
 | |
| // It is provided for historical compatibility.
 | |
| type Decoder interface {
 | |
| 	Decode(value string) error
 | |
| }
 | |
| 
 | |
| // Setter is implemented by types can self-deserialize values.
 | |
| // Any type that implements flag.Value also implements Setter.
 | |
| type Setter interface {
 | |
| 	Set(value string) error
 | |
| }
 | |
| 
 | |
| func (e *ParseError) Error() string {
 | |
| 	return fmt.Sprintf("envconfig.Process: assigning %[1]s to %[2]s: converting '%[3]s' to type %[4]s. details: %[5]s", e.KeyName, e.FieldName, e.Value, e.TypeName, e.Err)
 | |
| }
 | |
| 
 | |
| // varInfo maintains information about the configuration variable
 | |
| type varInfo struct {
 | |
| 	Name  string
 | |
| 	Alt   string
 | |
| 	Key   string
 | |
| 	Field reflect.Value
 | |
| 	Tags  reflect.StructTag
 | |
| }
 | |
| 
 | |
| // GatherInfo gathers information about the specified struct
 | |
| func gatherInfo(prefix string, spec interface{}) ([]varInfo, error) {
 | |
| 	s := reflect.ValueOf(spec)
 | |
| 
 | |
| 	if s.Kind() != reflect.Ptr {
 | |
| 		return nil, ErrInvalidSpecification
 | |
| 	}
 | |
| 	s = s.Elem()
 | |
| 	if s.Kind() != reflect.Struct {
 | |
| 		return nil, ErrInvalidSpecification
 | |
| 	}
 | |
| 	typeOfSpec := s.Type()
 | |
| 
 | |
| 	// over allocate an info array, we will extend if needed later
 | |
| 	infos := make([]varInfo, 0, s.NumField())
 | |
| 	for i := 0; i < s.NumField(); i++ {
 | |
| 		f := s.Field(i)
 | |
| 		ftype := typeOfSpec.Field(i)
 | |
| 		if !f.CanSet() || isTrue(ftype.Tag.Get("ignored")) {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		for f.Kind() == reflect.Ptr {
 | |
| 			if f.IsNil() {
 | |
| 				if f.Type().Elem().Kind() != reflect.Struct {
 | |
| 					// nil pointer to a non-struct: leave it alone
 | |
| 					break
 | |
| 				}
 | |
| 				// nil pointer to struct: create a zero instance
 | |
| 				f.Set(reflect.New(f.Type().Elem()))
 | |
| 			}
 | |
| 			f = f.Elem()
 | |
| 		}
 | |
| 
 | |
| 		// Capture information about the config variable
 | |
| 		info := varInfo{
 | |
| 			Name:  ftype.Name,
 | |
| 			Field: f,
 | |
| 			Tags:  ftype.Tag,
 | |
| 			Alt:   strings.ToUpper(ftype.Tag.Get("envconfig")),
 | |
| 		}
 | |
| 
 | |
| 		// Default to the field name as the env var name (will be upcased)
 | |
| 		info.Key = info.Name
 | |
| 
 | |
| 		// Best effort to un-pick camel casing as separate words
 | |
| 		if isTrue(ftype.Tag.Get("split_words")) {
 | |
| 			words := gatherRegexp.FindAllStringSubmatch(ftype.Name, -1)
 | |
| 			if len(words) > 0 {
 | |
| 				var name []string
 | |
| 				for _, words := range words {
 | |
| 					if m := acronymRegexp.FindStringSubmatch(words[0]); len(m) == 3 {
 | |
| 						name = append(name, m[1], m[2])
 | |
| 					} else {
 | |
| 						name = append(name, words[0])
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				info.Key = strings.Join(name, "_")
 | |
| 			}
 | |
| 		}
 | |
| 		if info.Alt != "" {
 | |
| 			info.Key = info.Alt
 | |
| 		}
 | |
| 		if prefix != "" {
 | |
| 			info.Key = fmt.Sprintf("%s_%s", prefix, info.Key)
 | |
| 		}
 | |
| 		info.Key = strings.ToUpper(info.Key)
 | |
| 		infos = append(infos, info)
 | |
| 
 | |
| 		if f.Kind() == reflect.Struct {
 | |
| 			// honor Decode if present
 | |
| 			if decoderFrom(f) == nil && setterFrom(f) == nil && textUnmarshaler(f) == nil && binaryUnmarshaler(f) == nil {
 | |
| 				innerPrefix := prefix
 | |
| 				if !ftype.Anonymous {
 | |
| 					innerPrefix = info.Key
 | |
| 				}
 | |
| 
 | |
| 				embeddedPtr := f.Addr().Interface()
 | |
| 				embeddedInfos, err := gatherInfo(innerPrefix, embeddedPtr)
 | |
| 				if err != nil {
 | |
| 					return nil, err
 | |
| 				}
 | |
| 				infos = append(infos[:len(infos)-1], embeddedInfos...)
 | |
| 
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return infos, nil
 | |
| }
 | |
| 
 | |
| // CheckDisallowed checks that no environment variables with the prefix are set
 | |
| // that we don't know how or want to parse. This is likely only meaningful with
 | |
| // a non-empty prefix.
 | |
| func CheckDisallowed(prefix string, spec interface{}) error {
 | |
| 	infos, err := gatherInfo(prefix, spec)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	vars := make(map[string]struct{})
 | |
| 	for _, info := range infos {
 | |
| 		vars[info.Key] = struct{}{}
 | |
| 	}
 | |
| 
 | |
| 	if prefix != "" {
 | |
| 		prefix = strings.ToUpper(prefix) + "_"
 | |
| 	}
 | |
| 
 | |
| 	for _, env := range os.Environ() {
 | |
| 		if !strings.HasPrefix(env, prefix) {
 | |
| 			continue
 | |
| 		}
 | |
| 		v := strings.SplitN(env, "=", 2)[0]
 | |
| 		if _, found := vars[v]; !found {
 | |
| 			return fmt.Errorf("unknown environment variable %s", v)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Process populates the specified struct based on environment variables
 | |
| func Process(prefix string, spec interface{}) error {
 | |
| 	infos, err := gatherInfo(prefix, spec)
 | |
| 
 | |
| 	for _, info := range infos {
 | |
| 
 | |
| 		// `os.Getenv` cannot differentiate between an explicitly set empty value
 | |
| 		// and an unset value. `os.LookupEnv` is preferred to `syscall.Getenv`,
 | |
| 		// but it is only available in go1.5 or newer. We're using Go build tags
 | |
| 		// here to use os.LookupEnv for >=go1.5
 | |
| 		value, ok := lookupEnv(info.Key)
 | |
| 		if !ok && info.Alt != "" {
 | |
| 			value, ok = lookupEnv(info.Alt)
 | |
| 		}
 | |
| 
 | |
| 		def := info.Tags.Get("default")
 | |
| 		if def != "" && !ok {
 | |
| 			value = def
 | |
| 		}
 | |
| 
 | |
| 		req := info.Tags.Get("required")
 | |
| 		if !ok && def == "" {
 | |
| 			if isTrue(req) {
 | |
| 				key := info.Key
 | |
| 				if info.Alt != "" {
 | |
| 					key = info.Alt
 | |
| 				}
 | |
| 				return fmt.Errorf("required key %s missing value", key)
 | |
| 			}
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		err = processField(value, info.Field)
 | |
| 		if err != nil {
 | |
| 			return &ParseError{
 | |
| 				KeyName:   info.Key,
 | |
| 				FieldName: info.Name,
 | |
| 				TypeName:  info.Field.Type().String(),
 | |
| 				Value:     value,
 | |
| 				Err:       err,
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| // MustProcess is the same as Process but panics if an error occurs
 | |
| func MustProcess(prefix string, spec interface{}) {
 | |
| 	if err := Process(prefix, spec); err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func processField(value string, field reflect.Value) error {
 | |
| 	typ := field.Type()
 | |
| 
 | |
| 	decoder := decoderFrom(field)
 | |
| 	if decoder != nil {
 | |
| 		return decoder.Decode(value)
 | |
| 	}
 | |
| 	// look for Set method if Decode not defined
 | |
| 	setter := setterFrom(field)
 | |
| 	if setter != nil {
 | |
| 		return setter.Set(value)
 | |
| 	}
 | |
| 
 | |
| 	if t := textUnmarshaler(field); t != nil {
 | |
| 		return t.UnmarshalText([]byte(value))
 | |
| 	}
 | |
| 
 | |
| 	if b := binaryUnmarshaler(field); b != nil {
 | |
| 		return b.UnmarshalBinary([]byte(value))
 | |
| 	}
 | |
| 
 | |
| 	if typ.Kind() == reflect.Ptr {
 | |
| 		typ = typ.Elem()
 | |
| 		if field.IsNil() {
 | |
| 			field.Set(reflect.New(typ))
 | |
| 		}
 | |
| 		field = field.Elem()
 | |
| 	}
 | |
| 
 | |
| 	switch typ.Kind() {
 | |
| 	case reflect.String:
 | |
| 		field.SetString(value)
 | |
| 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 | |
| 		var (
 | |
| 			val int64
 | |
| 			err error
 | |
| 		)
 | |
| 		if field.Kind() == reflect.Int64 && typ.PkgPath() == "time" && typ.Name() == "Duration" {
 | |
| 			var d time.Duration
 | |
| 			d, err = time.ParseDuration(value)
 | |
| 			val = int64(d)
 | |
| 		} else {
 | |
| 			val, err = strconv.ParseInt(value, 0, typ.Bits())
 | |
| 		}
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		field.SetInt(val)
 | |
| 	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
 | |
| 		val, err := strconv.ParseUint(value, 0, typ.Bits())
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		field.SetUint(val)
 | |
| 	case reflect.Bool:
 | |
| 		val, err := strconv.ParseBool(value)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		field.SetBool(val)
 | |
| 	case reflect.Float32, reflect.Float64:
 | |
| 		val, err := strconv.ParseFloat(value, typ.Bits())
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		field.SetFloat(val)
 | |
| 	case reflect.Slice:
 | |
| 		sl := reflect.MakeSlice(typ, 0, 0)
 | |
| 		if typ.Elem().Kind() == reflect.Uint8 {
 | |
| 			sl = reflect.ValueOf([]byte(value))
 | |
| 		} else if len(strings.TrimSpace(value)) != 0 {
 | |
| 			vals := strings.Split(value, ",")
 | |
| 			sl = reflect.MakeSlice(typ, len(vals), len(vals))
 | |
| 			for i, val := range vals {
 | |
| 				err := processField(val, sl.Index(i))
 | |
| 				if err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		field.Set(sl)
 | |
| 	case reflect.Map:
 | |
| 		mp := reflect.MakeMap(typ)
 | |
| 		if len(strings.TrimSpace(value)) != 0 {
 | |
| 			pairs := strings.Split(value, ",")
 | |
| 			for _, pair := range pairs {
 | |
| 				kvpair := strings.Split(pair, ":")
 | |
| 				if len(kvpair) != 2 {
 | |
| 					return fmt.Errorf("invalid map item: %q", pair)
 | |
| 				}
 | |
| 				k := reflect.New(typ.Key()).Elem()
 | |
| 				err := processField(kvpair[0], k)
 | |
| 				if err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 				v := reflect.New(typ.Elem()).Elem()
 | |
| 				err = processField(kvpair[1], v)
 | |
| 				if err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 				mp.SetMapIndex(k, v)
 | |
| 			}
 | |
| 		}
 | |
| 		field.Set(mp)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func interfaceFrom(field reflect.Value, fn func(interface{}, *bool)) {
 | |
| 	// it may be impossible for a struct field to fail this check
 | |
| 	if !field.CanInterface() {
 | |
| 		return
 | |
| 	}
 | |
| 	var ok bool
 | |
| 	fn(field.Interface(), &ok)
 | |
| 	if !ok && field.CanAddr() {
 | |
| 		fn(field.Addr().Interface(), &ok)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func decoderFrom(field reflect.Value) (d Decoder) {
 | |
| 	interfaceFrom(field, func(v interface{}, ok *bool) { d, *ok = v.(Decoder) })
 | |
| 	return d
 | |
| }
 | |
| 
 | |
| func setterFrom(field reflect.Value) (s Setter) {
 | |
| 	interfaceFrom(field, func(v interface{}, ok *bool) { s, *ok = v.(Setter) })
 | |
| 	return s
 | |
| }
 | |
| 
 | |
| func textUnmarshaler(field reflect.Value) (t encoding.TextUnmarshaler) {
 | |
| 	interfaceFrom(field, func(v interface{}, ok *bool) { t, *ok = v.(encoding.TextUnmarshaler) })
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| func binaryUnmarshaler(field reflect.Value) (b encoding.BinaryUnmarshaler) {
 | |
| 	interfaceFrom(field, func(v interface{}, ok *bool) { b, *ok = v.(encoding.BinaryUnmarshaler) })
 | |
| 	return b
 | |
| }
 | |
| 
 | |
| func isTrue(s string) bool {
 | |
| 	b, _ := strconv.ParseBool(s)
 | |
| 	return b
 | |
| }
 |