package validator import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "strings" "browser.local/platform/domain" ) const ( MaxLogBatchEntries = 512 MaxLogLineLength = 8192 MaxLogQueryLimit = 10000 ) func ValidateLogBatchIngest(batch domain.LogBatchIngest) error { var violations []string violations = appendRequired(violations, "runEndpointId", batch.RunEndpointID) violations = appendRequired(violations, "sessionToken", batch.SessionToken) violations = appendRequired(violations, "logStreamId", batch.LogStreamID) violations = appendRequired(violations, "serverInstanceId", batch.ServerInstanceID) violations = appendRequired(violations, "streamKey", batch.StreamKey) violations = appendRequired(violations, "checksum", batch.Checksum) if !validLogStreamSource(batch.Source) { violations = append(violations, "source is invalid") } if batch.FirstSeq == 0 || batch.LastSeq == 0 { violations = append(violations, "sequence range must be positive") } if batch.FirstSeq > batch.LastSeq { violations = append(violations, "firstSeq must not exceed lastSeq") } if batch.Compression != "" && batch.Compression != "none" { violations = append(violations, "compression is invalid") } if len(batch.Entries) == 0 { violations = append(violations, "entries must not be empty") } if len(batch.Entries) > MaxLogBatchEntries { violations = append(violations, fmt.Sprintf("entries must not exceed %d", MaxLogBatchEntries)) } if len(batch.Entries) > 0 { expectedCount := int(batch.LastSeq - batch.FirstSeq + 1) if expectedCount != len(batch.Entries) { violations = append(violations, "sequence range must match entry count") } } for i, entry := range batch.Entries { if entry.Seq != batch.FirstSeq+uint64(i) { violations = append(violations, fmt.Sprintf("entries[%d].seq must be contiguous", i)) } if strings.TrimSpace(entry.Line) == "" { violations = append(violations, fmt.Sprintf("entries[%d].line is required", i)) } if len(entry.Line) > MaxLogLineLength { violations = append(violations, fmt.Sprintf("entries[%d].line is too long", i)) } for key := range entry.Fields { if strings.TrimSpace(key) == "" { violations = append(violations, fmt.Sprintf("entries[%d].fields key is required", i)) } } } if batch.Checksum != "" { computed, err := LogEntriesChecksum(batch.Entries) if err != nil { violations = append(violations, "checksum cannot be computed") } else if batch.Checksum != computed && !logLineChecksumMatches(batch) { violations = append(violations, "checksum does not match entries") } } return finish(violations) } func ValidateLogStreamCursorQuery(query domain.LogStreamCursorQuery) error { var violations []string violations = appendRequired(violations, "logStreamId", query.LogStreamID) if query.Limit < 0 { violations = append(violations, "limit must not be negative") } if query.Limit > MaxLogQueryLimit { violations = append(violations, fmt.Sprintf("limit must not exceed %d", MaxLogQueryLimit)) } return finish(violations) } func ValidateRunLogStreamProgress(request domain.RunLogStreamProgress) error { var violations []string violations = appendRequired(violations, "runEndpointId", request.RunEndpointID) violations = appendRequired(violations, "sessionToken", request.SessionToken) violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID) violations = appendRequired(violations, "logStreamId", request.LogStreamID) return finish(violations) } func LogEntriesChecksum(entries []domain.LogEntry) (string, error) { stable := make([]logEntryChecksumBody, len(entries)) for i, entry := range entries { stable[i] = logEntryChecksumBody{ Seq: entry.Seq, Timestamp: entry.Timestamp.UTC().Format("2006-01-02T15:04:05.000000000Z07:00"), Level: entry.Level, Line: entry.Line, Fields: entry.Fields, Redacted: entry.Redacted, } } encoded, err := json.Marshal(stable) if err != nil { return "", err } sum := sha256.Sum256(encoded) return "sha256:" + hex.EncodeToString(sum[:]), nil } func logLineChecksumMatches(batch domain.LogBatchIngest) bool { if len(batch.Entries) != 1 { return false } return batch.Checksum == LogLineChecksum(batch.Entries[0].Line) } func LogLineChecksum(value string) string { sum := sha256.Sum256([]byte(value)) return "sha256:" + hex.EncodeToString(sum[:]) } type logEntryChecksumBody struct { Seq uint64 `json:"seq"` Timestamp string `json:"timestamp"` Level string `json:"level,omitempty"` Line string `json:"line"` Fields map[string]string `json:"fields,omitempty"` Redacted bool `json:"redacted"` }