feat: Production readiness improvements for WHOOSH council formation
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>
			
			
This commit is contained in:
		
							
								
								
									
										370
									
								
								vendor/github.com/ajg/form/decode.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										370
									
								
								vendor/github.com/ajg/form/decode.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,370 @@ | ||||
| // Copyright 2014 Alvaro J. Genial. All rights reserved. | ||||
| // Use of this source code is governed by a BSD-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package form | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"net/url" | ||||
| 	"reflect" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| // NewDecoder returns a new form Decoder. | ||||
| func NewDecoder(r io.Reader) *Decoder { | ||||
| 	return &Decoder{r, defaultDelimiter, defaultEscape, false, false} | ||||
| } | ||||
|  | ||||
| // Decoder decodes data from a form (application/x-www-form-urlencoded). | ||||
| type Decoder struct { | ||||
| 	r             io.Reader | ||||
| 	d             rune | ||||
| 	e             rune | ||||
| 	ignoreUnknown bool | ||||
| 	ignoreCase    bool | ||||
| } | ||||
|  | ||||
| // DelimitWith sets r as the delimiter used for composite keys by Decoder d and returns the latter; it is '.' by default. | ||||
| func (d *Decoder) DelimitWith(r rune) *Decoder { | ||||
| 	d.d = r | ||||
| 	return d | ||||
| } | ||||
|  | ||||
| // EscapeWith sets r as the escape used for delimiters (and to escape itself) by Decoder d and returns the latter; it is '\\' by default. | ||||
| func (d *Decoder) EscapeWith(r rune) *Decoder { | ||||
| 	d.e = r | ||||
| 	return d | ||||
| } | ||||
|  | ||||
| // Decode reads in and decodes form-encoded data into dst. | ||||
| func (d Decoder) Decode(dst interface{}) error { | ||||
| 	bs, err := ioutil.ReadAll(d.r) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	vs, err := url.ParseQuery(string(bs)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	v := reflect.ValueOf(dst) | ||||
| 	return d.decodeNode(v, parseValues(d.d, d.e, vs, canIndexOrdinally(v))) | ||||
| } | ||||
|  | ||||
| // IgnoreUnknownKeys if set to true it will make the Decoder ignore values | ||||
| // that are not found in the destination object instead of returning an error. | ||||
| func (d *Decoder) IgnoreUnknownKeys(ignoreUnknown bool) { | ||||
| 	d.ignoreUnknown = ignoreUnknown | ||||
| } | ||||
|  | ||||
| // IgnoreCase if set to true it will make the Decoder try to set values in the | ||||
| // destination object even if the case does not match. | ||||
| func (d *Decoder) IgnoreCase(ignoreCase bool) { | ||||
| 	d.ignoreCase = ignoreCase | ||||
| } | ||||
|  | ||||
| // DecodeString decodes src into dst. | ||||
| func (d Decoder) DecodeString(dst interface{}, src string) error { | ||||
| 	vs, err := url.ParseQuery(src) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	v := reflect.ValueOf(dst) | ||||
| 	return d.decodeNode(v, parseValues(d.d, d.e, vs, canIndexOrdinally(v))) | ||||
| } | ||||
|  | ||||
| // DecodeValues decodes vs into dst. | ||||
| func (d Decoder) DecodeValues(dst interface{}, vs url.Values) error { | ||||
| 	v := reflect.ValueOf(dst) | ||||
| 	return d.decodeNode(v, parseValues(d.d, d.e, vs, canIndexOrdinally(v))) | ||||
| } | ||||
|  | ||||
| // DecodeString decodes src into dst. | ||||
| func DecodeString(dst interface{}, src string) error { | ||||
| 	return NewDecoder(nil).DecodeString(dst, src) | ||||
| } | ||||
|  | ||||
| // DecodeValues decodes vs into dst. | ||||
| func DecodeValues(dst interface{}, vs url.Values) error { | ||||
| 	return NewDecoder(nil).DecodeValues(dst, vs) | ||||
| } | ||||
|  | ||||
| func (d Decoder) decodeNode(v reflect.Value, n node) (err error) { | ||||
| 	defer func() { | ||||
| 		if e := recover(); e != nil { | ||||
| 			err = fmt.Errorf("%v", e) | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	if v.Kind() == reflect.Slice { | ||||
| 		return fmt.Errorf("could not decode directly into slice; use pointer to slice") | ||||
| 	} | ||||
| 	d.decodeValue(v, n) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (d Decoder) decodeValue(v reflect.Value, x interface{}) { | ||||
| 	t := v.Type() | ||||
| 	k := v.Kind() | ||||
|  | ||||
| 	if k == reflect.Ptr && v.IsNil() { | ||||
| 		v.Set(reflect.New(t.Elem())) | ||||
| 	} | ||||
|  | ||||
| 	if unmarshalValue(v, x) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	empty := isEmpty(x) | ||||
|  | ||||
| 	switch k { | ||||
| 	case reflect.Ptr: | ||||
| 		d.decodeValue(v.Elem(), x) | ||||
| 		return | ||||
| 	case reflect.Interface: | ||||
| 		if !v.IsNil() { | ||||
| 			d.decodeValue(v.Elem(), x) | ||||
| 			return | ||||
|  | ||||
| 		} else if empty { | ||||
| 			return // Allow nil interfaces only if empty. | ||||
| 		} else { | ||||
| 			panic("form: cannot decode non-empty value into into nil interface") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if empty { | ||||
| 		v.Set(reflect.Zero(t)) // Treat the empty string as the zero value. | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	switch k { | ||||
| 	case reflect.Struct: | ||||
| 		if t.ConvertibleTo(timeType) { | ||||
| 			d.decodeTime(v, x) | ||||
| 		} else if t.ConvertibleTo(urlType) { | ||||
| 			d.decodeURL(v, x) | ||||
| 		} else { | ||||
| 			d.decodeStruct(v, x) | ||||
| 		} | ||||
| 	case reflect.Slice: | ||||
| 		d.decodeSlice(v, x) | ||||
| 	case reflect.Array: | ||||
| 		d.decodeArray(v, x) | ||||
| 	case reflect.Map: | ||||
| 		d.decodeMap(v, x) | ||||
| 	case reflect.Invalid, reflect.Uintptr, reflect.UnsafePointer, reflect.Chan, reflect.Func: | ||||
| 		panic(t.String() + " has unsupported kind " + k.String()) | ||||
| 	default: | ||||
| 		d.decodeBasic(v, x) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (d Decoder) decodeStruct(v reflect.Value, x interface{}) { | ||||
| 	t := v.Type() | ||||
| 	for k, c := range getNode(x) { | ||||
| 		if f, ok := findField(v, k, d.ignoreCase); !ok && k == "" { | ||||
| 			panic(getString(x) + " cannot be decoded as " + t.String()) | ||||
| 		} else if !ok { | ||||
| 			if !d.ignoreUnknown { | ||||
| 				panic(k + " doesn't exist in " + t.String()) | ||||
| 			} | ||||
| 		} else if !f.CanSet() { | ||||
| 			panic(k + " cannot be set in " + t.String()) | ||||
| 		} else { | ||||
| 			d.decodeValue(f, c) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (d Decoder) decodeMap(v reflect.Value, x interface{}) { | ||||
| 	t := v.Type() | ||||
| 	if v.IsNil() { | ||||
| 		v.Set(reflect.MakeMap(t)) | ||||
| 	} | ||||
| 	for k, c := range getNode(x) { | ||||
| 		i := reflect.New(t.Key()).Elem() | ||||
| 		d.decodeValue(i, k) | ||||
|  | ||||
| 		w := v.MapIndex(i) | ||||
| 		if w.IsValid() { // We have an actual element value to decode into. | ||||
| 			if w.Kind() == reflect.Interface { | ||||
| 				w = w.Elem() | ||||
| 			} | ||||
| 			w = reflect.New(w.Type()).Elem() | ||||
| 		} else if t.Elem().Kind() != reflect.Interface { // The map's element type is concrete. | ||||
| 			w = reflect.New(t.Elem()).Elem() | ||||
| 		} else { | ||||
| 			// The best we can do here is to decode as either a string (for scalars) or a map[string]interface {} (for the rest). | ||||
| 			// We could try to guess the type based on the string (e.g. true/false => bool) but that'll get ugly fast, | ||||
| 			// especially if we have to guess the kind (slice vs. array vs. map) and index type (e.g. string, int, etc.) | ||||
| 			switch c.(type) { | ||||
| 			case node: | ||||
| 				w = reflect.MakeMap(stringMapType) | ||||
| 			case string: | ||||
| 				w = reflect.New(stringType).Elem() | ||||
| 			default: | ||||
| 				panic("value is neither node nor string") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		d.decodeValue(w, c) | ||||
| 		v.SetMapIndex(i, w) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (d Decoder) decodeArray(v reflect.Value, x interface{}) { | ||||
| 	t := v.Type() | ||||
| 	for k, c := range getNode(x) { | ||||
| 		i, err := strconv.Atoi(k) | ||||
| 		if err != nil { | ||||
| 			panic(k + " is not a valid index for type " + t.String()) | ||||
| 		} | ||||
| 		if l := v.Len(); i >= l { | ||||
| 			panic("index is above array size") | ||||
| 		} | ||||
| 		d.decodeValue(v.Index(i), c) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (d Decoder) decodeSlice(v reflect.Value, x interface{}) { | ||||
| 	t := v.Type() | ||||
| 	if t.Elem().Kind() == reflect.Uint8 { | ||||
| 		// Allow, but don't require, byte slices to be encoded as a single string. | ||||
| 		if s, ok := x.(string); ok { | ||||
| 			v.SetBytes([]byte(s)) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// NOTE: Implicit indexing is currently done at the parseValues level, | ||||
| 	//       so if if an implicitKey reaches here it will always replace the last. | ||||
| 	implicit := 0 | ||||
| 	for k, c := range getNode(x) { | ||||
| 		var i int | ||||
| 		if k == implicitKey { | ||||
| 			i = implicit | ||||
| 			implicit++ | ||||
| 		} else { | ||||
| 			explicit, err := strconv.Atoi(k) | ||||
| 			if err != nil { | ||||
| 				panic(k + " is not a valid index for type " + t.String()) | ||||
| 			} | ||||
| 			i = explicit | ||||
| 			implicit = explicit + 1 | ||||
| 		} | ||||
| 		// "Extend" the slice if it's too short. | ||||
| 		if l := v.Len(); i >= l { | ||||
| 			delta := i - l + 1 | ||||
| 			v.Set(reflect.AppendSlice(v, reflect.MakeSlice(t, delta, delta))) | ||||
| 		} | ||||
| 		d.decodeValue(v.Index(i), c) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (d Decoder) decodeBasic(v reflect.Value, x interface{}) { | ||||
| 	t := v.Type() | ||||
| 	switch k, s := t.Kind(), getString(x); k { | ||||
| 	case reflect.Bool: | ||||
| 		if b, e := strconv.ParseBool(s); e == nil { | ||||
| 			v.SetBool(b) | ||||
| 		} else { | ||||
| 			panic("could not parse bool from " + strconv.Quote(s)) | ||||
| 		} | ||||
| 	case reflect.Int, | ||||
| 		reflect.Int8, | ||||
| 		reflect.Int16, | ||||
| 		reflect.Int32, | ||||
| 		reflect.Int64: | ||||
| 		if i, e := strconv.ParseInt(s, 10, 64); e == nil { | ||||
| 			v.SetInt(i) | ||||
| 		} else { | ||||
| 			panic("could not parse int from " + strconv.Quote(s)) | ||||
| 		} | ||||
| 	case reflect.Uint, | ||||
| 		reflect.Uint8, | ||||
| 		reflect.Uint16, | ||||
| 		reflect.Uint32, | ||||
| 		reflect.Uint64: | ||||
| 		if u, e := strconv.ParseUint(s, 10, 64); e == nil { | ||||
| 			v.SetUint(u) | ||||
| 		} else { | ||||
| 			panic("could not parse uint from " + strconv.Quote(s)) | ||||
| 		} | ||||
| 	case reflect.Float32, | ||||
| 		reflect.Float64: | ||||
| 		if f, e := strconv.ParseFloat(s, 64); e == nil { | ||||
| 			v.SetFloat(f) | ||||
| 		} else { | ||||
| 			panic("could not parse float from " + strconv.Quote(s)) | ||||
| 		} | ||||
| 	case reflect.Complex64, | ||||
| 		reflect.Complex128: | ||||
| 		var c complex128 | ||||
| 		if n, err := fmt.Sscanf(s, "%g", &c); n == 1 && err == nil { | ||||
| 			v.SetComplex(c) | ||||
| 		} else { | ||||
| 			panic("could not parse complex from " + strconv.Quote(s)) | ||||
| 		} | ||||
| 	case reflect.String: | ||||
| 		v.SetString(s) | ||||
| 	default: | ||||
| 		panic(t.String() + " has unsupported kind " + k.String()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (d Decoder) decodeTime(v reflect.Value, x interface{}) { | ||||
| 	t := v.Type() | ||||
| 	s := getString(x) | ||||
| 	// TODO: Find a more efficient way to do this. | ||||
| 	for _, f := range allowedTimeFormats { | ||||
| 		if p, err := time.Parse(f, s); err == nil { | ||||
| 			v.Set(reflect.ValueOf(p).Convert(v.Type())) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	panic("cannot decode string `" + s + "` as " + t.String()) | ||||
| } | ||||
|  | ||||
| func (d Decoder) decodeURL(v reflect.Value, x interface{}) { | ||||
| 	t := v.Type() | ||||
| 	s := getString(x) | ||||
| 	if u, err := url.Parse(s); err == nil { | ||||
| 		v.Set(reflect.ValueOf(*u).Convert(v.Type())) | ||||
| 		return | ||||
| 	} | ||||
| 	panic("cannot decode string `" + s + "` as " + t.String()) | ||||
| } | ||||
|  | ||||
| var allowedTimeFormats = []string{ | ||||
| 	"2006-01-02T15:04:05.999999999Z07:00", | ||||
| 	"2006-01-02T15:04:05.999999999Z07", | ||||
| 	"2006-01-02T15:04:05.999999999Z", | ||||
| 	"2006-01-02T15:04:05.999999999", | ||||
| 	"2006-01-02T15:04:05Z07:00", | ||||
| 	"2006-01-02T15:04:05Z07", | ||||
| 	"2006-01-02T15:04:05Z", | ||||
| 	"2006-01-02T15:04:05", | ||||
| 	"2006-01-02T15:04Z", | ||||
| 	"2006-01-02T15:04", | ||||
| 	"2006-01-02T15Z", | ||||
| 	"2006-01-02T15", | ||||
| 	"2006-01-02", | ||||
| 	"2006-01", | ||||
| 	"2006", | ||||
| 	"15:04:05.999999999Z07:00", | ||||
| 	"15:04:05.999999999Z07", | ||||
| 	"15:04:05.999999999Z", | ||||
| 	"15:04:05.999999999", | ||||
| 	"15:04:05Z07:00", | ||||
| 	"15:04:05Z07", | ||||
| 	"15:04:05Z", | ||||
| 	"15:04:05", | ||||
| 	"15:04Z", | ||||
| 	"15:04", | ||||
| 	"15Z", | ||||
| 	"15", | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Claude Code
					Claude Code