Files
OmniRoute/open-sse/services/claudeCodeObfuscation.ts
diegosouzapw 3b4e7b0e5f feat(claude-code): PR #1188 — Native parity with CCH signing, tool remapping, and API constraints
Squash merge of feat/claude-code-native-parity into release/v3.6.5.

## Wiring

1. base.ts: CCH xxHash64 body signing for anthropic-compatible-cc-* providers,
   applied after CLI fingerprint ordering so the hash covers the final bytes
   sent upstream.

2. chatCore.ts: After buildClaudeCodeCompatibleRequest(), applies synchronous
   parity pipeline steps:
   - remapToolNamesInRequest() — TitleCase tool name mapping (14 tools)
   - enforceThinkingTemperature() — temperature=1 when thinking active
   - disableThinkingIfToolChoiceForced() — remove thinking on forced tool_choice
   Cache-control limit enforcement intentionally omitted from chatCore because
   the billing-header system block counts toward the 4-block cap and would strip
   legitimate client cache markers.

3. xxhash-wasm: installed (was declared in package.json by PR but not installed).

## Test updates

- tests/unit/claude-code-parity.test.mjs: 25 new tests covering CCH signing,
  fingerprint computation, tool remapping, and API constraints.
- tests/unit/cc-compatible-provider.test.mjs: fixed pre-existing assertion that
  expected no cache_control on system blocks (billing header now carries ephemeral
  per PR #1188 design).

Closes #1188
2026-04-12 22:41:12 -03:00

78 lines
2.1 KiB
TypeScript

/**
* 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);
}
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
const regex = new RegExp(escapeRegex(word), "gi");
result = result.replace(regex, (match) => obfuscateWord(match));
}
return result;
}
export function obfuscateInBody(body: Record<string, unknown>): void {
const messages = body.messages as Array<Record<string, unknown>> | undefined;
if (!Array.isArray(messages)) return;
for (const msg of messages) {
if (String(msg.role) !== "user") continue;
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);
}
}
}
}
}
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}