Files
doormile_backend/internal/ai/playground/redact.go

79 lines
1.9 KiB
Go

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
}