package playground import ( "encoding/json" "regexp" "strings" ) // Redacted replaces every value Redact removes. const Redacted = "[redacted]" // piiKeys: a field whose name contains one of these is personal or free text // (free text is where people type phone numbers and addresses). var piiKeys = []string{ "name", "phone", "mobile", "email", "address", "landmark", "otp", "contact", "note", "remark", "instruction", "description", "reason", "comment", } var ( emailLike = regexp.MustCompile(`[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}`) // An Indian mobile (10 digits from 6-9, optional +91). Narrow on purpose: // a looser digit-run pattern also masks dates like "2026-09-29 12". phoneLike = regexp.MustCompile(`(?:\+?91[\s-]?)?\b[6-9]\d{4}[\s-]?\d{5}\b`) ) // Redact returns a copy of v, as plain JSON values, with personal data // removed: values under personal keys are replaced, and any remaining string // that contains an email or a phone-like number has it masked. // // Executors already select only non-personal columns; this is the backstop // that makes a later column addition safe by default. func Redact(v any) any { b, err := json.Marshal(v) if err != nil { return nil } var generic any if err := json.Unmarshal(b, &generic); err != nil { return nil } return redactValue(generic) } func redactValue(v any) any { switch t := v.(type) { case map[string]any: out := make(map[string]any, len(t)) for k, val := range t { if isPIIKey(k) && val != nil { out[k] = Redacted continue } out[k] = redactValue(val) } return out case []any: out := make([]any, len(t)) for i, val := range t { out[i] = redactValue(val) } return out case string: s := emailLike.ReplaceAllString(t, Redacted) return phoneLike.ReplaceAllString(s, Redacted) default: return v } } func isPIIKey(k string) bool { k = strings.ToLower(k) for _, p := range piiKeys { if strings.Contains(k, p) { return true } } return false }