package outcome import ( "crypto/sha256" "encoding/json" "errors" "regexp" "time" ) const ( maximumIdentifierBytes = 128 MaxBatchesPerStream = 16 ) var ( ErrInvalidBatch = errors.New("invalid proxy outcome batch") identifier = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`) ) // Stage identifies the furthest proxy-processing boundary reached by a // request. It is deliberately independent of a particular transport error. type Stage uint8 const ( StageUnspecified Stage = iota StageDial StageProxyHandshake StageResponseHeaders StageTunnel ) // ErrorClass is a bounded category suitable for aggregation and metrics. // It must never contain raw connection, target, or credential details. type ErrorClass string const ( ErrorClassDial ErrorClass = "dial" ErrorClassTimeout ErrorClass = "timeout" ErrorClassProxyResponse ErrorClass = "proxy_response" ErrorClassHandshake ErrorClass = "handshake" ErrorClassRelay ErrorClass = "relay" ErrorClassCanceled ErrorClass = "canceled" ErrorClassInternal ErrorClass = "internal" ) type Event struct { ProxyID string RoutingName string Stage Stage Success bool ErrorClass ErrorClass Latency time.Duration ObservedAt time.Time } type Batch struct { WorkerID string SessionID string Sequence uint64 Events []Event } // NormalizeBatch validates one bounded report, deep-copies it, and returns a // digest used by the Controller's durable idempotency fence. func NormalizeBatch(batch Batch, maximumEvents int) (Batch, [sha256.Size]byte, error) { if !validIdentifier(batch.WorkerID) || !validIdentifier(batch.SessionID) || batch.Sequence == 0 || maximumEvents <= 0 || len(batch.Events) == 0 || len(batch.Events) > maximumEvents { return Batch{}, [sha256.Size]byte{}, ErrInvalidBatch } normalized := batch normalized.Events = append([]Event(nil), batch.Events...) for index := range normalized.Events { event := &normalized.Events[index] if !validIdentifier(event.ProxyID) || (event.RoutingName != "" && !validIdentifier(event.RoutingName)) || !validStage(event.Stage) || event.Latency < 0 || event.ObservedAt.IsZero() || (event.Success && event.ErrorClass != "") || (!event.Success && !validErrorClass(event.ErrorClass)) { return Batch{}, [sha256.Size]byte{}, ErrInvalidBatch } event.ObservedAt = event.ObservedAt.UTC() } payload, err := json.Marshal(normalized) if err != nil { return Batch{}, [sha256.Size]byte{}, ErrInvalidBatch } return normalized, sha256.Sum256(payload), nil } func validIdentifier(value string) bool { return len(value) <= maximumIdentifierBytes && identifier.MatchString(value) } func validStage(stage Stage) bool { switch stage { case StageDial, StageProxyHandshake, StageResponseHeaders, StageTunnel: return true default: return false } } func validErrorClass(class ErrorClass) bool { switch class { case ErrorClassDial, ErrorClassTimeout, ErrorClassProxyResponse, ErrorClassHandshake, ErrorClassRelay, ErrorClassCanceled, ErrorClassInternal: return true default: return false } }