File size: 3,747 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
 * Sensitive word obfuscation for Claude Code requests.
 *
 * Obfuscates configurable words in user messages to prevent detection
 * by upstream content filters. Uses zero-width characters to break
 * pattern matching while preserving readability.
 */

// Unicode zero-width joiner inserted between characters
const ZWJ = "\u200d";

const DEFAULT_SENSITIVE_WORDS = [
  "opencode",
  "open-code",
  "cline",
  "roo-cline",
  "roo_cline",
  "cursor",
  "windsurf",
  "aider",
  "continue.dev",
  "copilot",
  "avante",
  "codecompanion",
];

let sensitiveWords = [...DEFAULT_SENSITIVE_WORDS];

export function setSensitiveWords(words: string[]): void {
  sensitiveWords = words.length > 0 ? words : [...DEFAULT_SENSITIVE_WORDS];
}

export function getSensitiveWords(): string[] {
  return [...sensitiveWords];
}

function obfuscateWord(word: string): string {
  if (word.length <= 1) return word;
  // Insert ZWJ after first character
  return word[0] + ZWJ + word.slice(1);
}

// Per-word regex cache — obfuscateSensitiveWords recompiles one RegExp per word on every
// request body otherwise. Bounded by distinct configured words; global regexes are safe to
// reuse because String.replace resets lastIndex.
const _obfuscationRegexCache = new Map<string, RegExp>();
function getObfuscationRegex(word: string): RegExp {
  let regex = _obfuscationRegexCache.get(word);
  if (!regex) {
    if (_obfuscationRegexCache.size > 2000) _obfuscationRegexCache.clear();
    regex = new RegExp(escapeRegex(word), "gi");
    _obfuscationRegexCache.set(word, regex);
  }
  return regex;
}

export function obfuscateSensitiveWords(text: string): string {
  if (!text || sensitiveWords.length === 0) return text;

  let result = text;
  for (const word of sensitiveWords) {
    if (!word) continue;
    // Case-insensitive replacement (cached: see getObfuscationRegex)
    const regex = getObfuscationRegex(word);
    result = result.replace(regex, (match) => obfuscateWord(match));
  }
  return result;
}

export function obfuscateInBody(body: Record<string, unknown>): void {
  // System prompt (Claude format: string or array of blocks)
  if (typeof body.system === "string") {
    body.system = obfuscateSensitiveWords(body.system);
  } else if (Array.isArray(body.system)) {
    for (const block of body.system as Array<Record<string, unknown>>) {
      if (typeof block.text === "string") {
        block.text = obfuscateSensitiveWords(block.text);
      }
    }
  }

  // Messages (all roles, not just user — system/assistant may also contain sensitive words)
  const messages = body.messages as Array<Record<string, unknown>> | undefined;
  if (Array.isArray(messages)) {
    for (const msg of messages) {
      const content = msg.content;
      if (typeof content === "string") {
        msg.content = obfuscateSensitiveWords(content);
      } else if (Array.isArray(content)) {
        for (const block of content as Array<Record<string, unknown>>) {
          if (typeof block.text === "string") {
            block.text = obfuscateSensitiveWords(block.text);
          }
        }
      }
    }
  }

  // Tool descriptions (may contain URLs or names like "opencode")
  const tools = body.tools as Array<Record<string, unknown>> | undefined;
  if (Array.isArray(tools)) {
    for (const tool of tools) {
      if (typeof tool.description === "string") {
        tool.description = obfuscateSensitiveWords(tool.description);
      }
      const fn = tool.function as Record<string, unknown> | undefined;
      if (fn && typeof fn.description === "string") {
        fn.description = obfuscateSensitiveWords(fn.description);
      }
    }
  }
}

function escapeRegex(str: string): string {
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}