Spaces:
Build error
Build error
| /** | |
| * Parseltongue - Input Obfuscation Engine | |
| * Detects trigger words likely to cause model refusals and applies | |
| * obfuscation techniques to bypass content filters. | |
| * | |
| * Techniques: | |
| * - Leetspeak (1337): a→4, e→3, i→1, o→0, etc. | |
| * - Unicode homoglyphs: a→а (cyrillic), e→е, etc. | |
| * - Zero-width joiners: invisible characters between letters | |
| * - Mixed case disruption: alternating caps | |
| * - Phonetic substitution: replacing with phonetically similar words | |
| */ | |
| export type ObfuscationTechnique = | |
| | 'leetspeak' | |
| | 'unicode' | |
| | 'zwj' // zero-width joiners | |
| | 'mixedcase' | |
| | 'phonetic' | |
| | 'random' // randomly mix techniques | |
| export interface ParseltongueConfig { | |
| enabled: boolean | |
| technique: ObfuscationTechnique | |
| intensity: 'light' | 'medium' | 'heavy' // how many characters to transform | |
| customTriggers: string[] // user-defined trigger words | |
| } | |
| export interface ParseltongueResult { | |
| originalText: string | |
| transformedText: string | |
| triggersFound: string[] | |
| techniqueUsed: ObfuscationTechnique | |
| transformations: Array<{ | |
| original: string | |
| transformed: string | |
| technique: ObfuscationTechnique | |
| }> | |
| } | |
| // Common trigger words/phrases that often cause refusals | |
| // These are words that models tend to flag, not inherently bad content | |
| const DEFAULT_TRIGGERS: string[] = [ | |
| // Action words | |
| 'hack', 'exploit', 'bypass', 'crack', 'break', 'attack', 'penetrate', | |
| 'inject', 'manipulate', 'override', 'disable', 'circumvent', 'evade', | |
| // Security terms | |
| 'malware', 'virus', 'trojan', 'payload', 'shellcode', 'rootkit', | |
| 'keylogger', 'backdoor', 'vulnerability', 'exploit', | |
| // Sensitive topics | |
| 'weapon', 'bomb', 'explosive', 'poison', 'drug', 'synthesize', | |
| // System terms | |
| 'jailbreak', 'unlock', 'root', 'sudo', 'admin', 'privilege', | |
| // Social engineering | |
| 'phishing', 'scam', 'impersonate', 'deceive', 'fraud', | |
| // Content flags | |
| 'nsfw', 'explicit', 'uncensored', 'unfiltered', 'unrestricted', | |
| // AI-specific | |
| 'ignore', 'disregard', 'forget', 'pretend', 'roleplay', | |
| 'character', 'act as', 'you are now', 'new identity' | |
| ] | |
| // Leetspeak mappings (multiple options per character) | |
| const LEET_MAP: Record<string, string[]> = { | |
| 'a': ['4', '@', '∂', 'λ'], | |
| 'b': ['8', '|3', 'ß', '13'], | |
| 'c': ['(', '<', '¢', '©'], | |
| 'd': ['|)', '|>', 'đ'], | |
| 'e': ['3', '€', '£', '∑'], | |
| 'f': ['|=', 'ƒ', 'ph'], | |
| 'g': ['9', '6', '&'], | |
| 'h': ['#', '|-|', '}{'], | |
| 'i': ['1', '!', '|', '¡'], | |
| 'j': ['_|', ']', '¿'], | |
| 'k': ['|<', '|{', 'κ'], | |
| 'l': ['1', '|', '£', '|_'], | |
| 'm': ['|V|', '/\\/\\', 'µ'], | |
| 'n': ['|\\|', '/\\/', 'η'], | |
| 'o': ['0', '()', '°', 'ø'], | |
| 'p': ['|*', '|>', 'þ'], | |
| 'q': ['0_', '()_', 'ℚ'], | |
| 'r': ['|2', '®', '12'], | |
| 's': ['5', '$', '§', '∫'], | |
| 't': ['7', '+', '†', '⊤'], | |
| 'u': ['|_|', 'µ', 'ü'], | |
| 'v': ['\\/', '√'], | |
| 'w': ['\\/\\/', 'vv', 'ω'], | |
| 'x': ['><', '×', '}{'], | |
| 'y': ['`/', '¥', 'γ'], | |
| 'z': ['2', '7_', 'ℤ'] | |
| } | |
| // Unicode homoglyphs (visually similar but different Unicode codepoints) | |
| const UNICODE_HOMOGLYPHS: Record<string, string[]> = { | |
| 'a': ['а', 'ɑ', 'α', 'a'], // cyrillic а, latin alpha, etc. | |
| 'b': ['Ь', 'b', 'ḅ'], | |
| 'c': ['с', 'ϲ', 'ⅽ', 'c'], // cyrillic с | |
| 'd': ['ԁ', 'ⅾ', 'd'], | |
| 'e': ['е', 'ė', 'ẹ', 'e'], // cyrillic е | |
| 'f': ['ƒ', 'f'], | |
| 'g': ['ɡ', 'g'], | |
| 'h': ['һ', 'ḥ', 'h'], // cyrillic һ | |
| 'i': ['і', 'ι', 'i'], // cyrillic і, greek iota | |
| 'j': ['ϳ', 'j'], | |
| 'k': ['κ', 'k'], | |
| 'l': ['ӏ', 'ⅼ', 'l'], // cyrillic palochka | |
| 'm': ['м', 'm'], | |
| 'n': ['ո', 'n'], | |
| 'o': ['о', 'ο', 'o'], // cyrillic о, greek omicron | |
| 'p': ['р', 'ρ', 'p'], // cyrillic р, greek rho | |
| 's': ['ѕ', 's'], // cyrillic ѕ | |
| 't': ['τ', 't'], | |
| 'u': ['υ', 'u'], | |
| 'v': ['ν', 'v'], // greek nu | |
| 'w': ['ѡ', 'w'], | |
| 'x': ['х', 'x'], // cyrillic х | |
| 'y': ['у', 'γ', 'y'], // cyrillic у | |
| 'z': ['ᴢ', 'z'] | |
| } | |
| // Zero-width characters for invisible insertion | |
| const ZW_CHARS = [ | |
| '\u200B', // Zero-width space | |
| '\u200C', // Zero-width non-joiner | |
| '\u200D', // Zero-width joiner | |
| '\uFEFF', // Zero-width no-break space | |
| ] | |
| /** | |
| * Apply leetspeak transformation to a word | |
| */ | |
| function applyLeetspeak(word: string, intensity: 'light' | 'medium' | 'heavy'): string { | |
| const chars = word.split('') | |
| const transformCount = intensity === 'light' ? 1 : intensity === 'medium' ? Math.ceil(chars.length / 2) : chars.length | |
| // Get indices to transform (spread throughout the word) | |
| const indices: number[] = [] | |
| const step = Math.max(1, Math.floor(chars.length / transformCount)) | |
| for (let i = 0; i < chars.length && indices.length < transformCount; i += step) { | |
| if (LEET_MAP[chars[i].toLowerCase()]) { | |
| indices.push(i) | |
| } | |
| } | |
| // If we didn't get enough, grab any remaining transformable chars | |
| if (indices.length < transformCount) { | |
| for (let i = 0; i < chars.length && indices.length < transformCount; i++) { | |
| if (!indices.includes(i) && LEET_MAP[chars[i].toLowerCase()]) { | |
| indices.push(i) | |
| } | |
| } | |
| } | |
| // Apply transformations | |
| for (const i of indices) { | |
| const char = chars[i].toLowerCase() | |
| if (LEET_MAP[char]) { | |
| const options = LEET_MAP[char] | |
| chars[i] = options[Math.floor(Math.random() * options.length)] | |
| } | |
| } | |
| return chars.join('') | |
| } | |
| /** | |
| * Apply unicode homoglyph transformation | |
| */ | |
| function applyUnicode(word: string, intensity: 'light' | 'medium' | 'heavy'): string { | |
| const chars = word.split('') | |
| const transformCount = intensity === 'light' ? 1 : intensity === 'medium' ? Math.ceil(chars.length / 2) : chars.length | |
| const indices: number[] = [] | |
| for (let i = 0; i < chars.length && indices.length < transformCount; i++) { | |
| if (UNICODE_HOMOGLYPHS[chars[i].toLowerCase()]) { | |
| indices.push(i) | |
| } | |
| } | |
| for (const i of indices) { | |
| const char = chars[i].toLowerCase() | |
| if (UNICODE_HOMOGLYPHS[char]) { | |
| const options = UNICODE_HOMOGLYPHS[char] | |
| const replacement = options[Math.floor(Math.random() * options.length)] | |
| // Preserve case | |
| chars[i] = chars[i] === chars[i].toUpperCase() ? replacement.toUpperCase() : replacement | |
| } | |
| } | |
| return chars.join('') | |
| } | |
| /** | |
| * Insert zero-width characters between letters | |
| */ | |
| function applyZWJ(word: string, intensity: 'light' | 'medium' | 'heavy'): string { | |
| const chars = word.split('') | |
| const insertCount = intensity === 'light' ? 1 : intensity === 'medium' ? Math.ceil(chars.length / 2) : chars.length - 1 | |
| const result: string[] = [] | |
| let insertions = 0 | |
| for (let i = 0; i < chars.length; i++) { | |
| result.push(chars[i]) | |
| if (i < chars.length - 1 && insertions < insertCount) { | |
| const zwChar = ZW_CHARS[Math.floor(Math.random() * ZW_CHARS.length)] | |
| result.push(zwChar) | |
| insertions++ | |
| } | |
| } | |
| return result.join('') | |
| } | |
| /** | |
| * Apply mixed case disruption | |
| */ | |
| function applyMixedCase(word: string, intensity: 'light' | 'medium' | 'heavy'): string { | |
| const chars = word.split('') | |
| if (intensity === 'light') { | |
| // Just capitalize one random letter | |
| const idx = Math.floor(Math.random() * chars.length) | |
| chars[idx] = chars[idx].toUpperCase() | |
| } else if (intensity === 'medium') { | |
| // Alternating case | |
| for (let i = 0; i < chars.length; i++) { | |
| chars[i] = i % 2 === 0 ? chars[i].toLowerCase() : chars[i].toUpperCase() | |
| } | |
| } else { | |
| // Random case | |
| for (let i = 0; i < chars.length; i++) { | |
| chars[i] = Math.random() > 0.5 ? chars[i].toUpperCase() : chars[i].toLowerCase() | |
| } | |
| } | |
| return chars.join('') | |
| } | |
| /** | |
| * Apply phonetic substitution | |
| */ | |
| function applyPhonetic(word: string): string { | |
| // Common phonetic substitutions | |
| const substitutions: [RegExp, string][] = [ | |
| [/ph/gi, 'f'], | |
| [/ck/gi, 'k'], | |
| [/x/gi, 'ks'], | |
| [/qu/gi, 'kw'], | |
| [/c(?=[eiy])/gi, 's'], // soft c | |
| [/c/gi, 'k'], // hard c | |
| ] | |
| let result = word | |
| for (const [pattern, replacement] of substitutions) { | |
| result = result.replace(pattern, replacement) | |
| } | |
| return result | |
| } | |
| /** | |
| * Apply a random mix of techniques | |
| */ | |
| function applyRandom(word: string, intensity: 'light' | 'medium' | 'heavy'): string { | |
| const techniques = [applyLeetspeak, applyUnicode, applyZWJ, applyMixedCase] | |
| const technique = techniques[Math.floor(Math.random() * techniques.length)] | |
| return technique(word, intensity) | |
| } | |
| /** | |
| * Main obfuscation function for a single word | |
| */ | |
| function obfuscateWord( | |
| word: string, | |
| technique: ObfuscationTechnique, | |
| intensity: 'light' | 'medium' | 'heavy' | |
| ): string { | |
| switch (technique) { | |
| case 'leetspeak': | |
| return applyLeetspeak(word, intensity) | |
| case 'unicode': | |
| return applyUnicode(word, intensity) | |
| case 'zwj': | |
| return applyZWJ(word, intensity) | |
| case 'mixedcase': | |
| return applyMixedCase(word, intensity) | |
| case 'phonetic': | |
| return applyPhonetic(word) | |
| case 'random': | |
| return applyRandom(word, intensity) | |
| default: | |
| return word | |
| } | |
| } | |
| /** | |
| * Find all trigger words in the input text | |
| */ | |
| export function detectTriggers( | |
| text: string, | |
| customTriggers: string[] = [] | |
| ): string[] { | |
| const allTriggers = [...DEFAULT_TRIGGERS, ...customTriggers] | |
| const found: string[] = [] | |
| const lowerText = text.toLowerCase() | |
| for (const trigger of allTriggers) { | |
| // Check for whole word match (with word boundaries) | |
| const regex = new RegExp(`\\b${trigger.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'gi') | |
| if (regex.test(lowerText)) { | |
| found.push(trigger) | |
| } | |
| } | |
| return Array.from(new Set(found)) // Remove duplicates | |
| } | |
| /** | |
| * Main parseltongue transformation function | |
| * Detects trigger words and obfuscates them | |
| */ | |
| export function applyParseltongue( | |
| text: string, | |
| config: ParseltongueConfig | |
| ): ParseltongueResult { | |
| if (!config.enabled) { | |
| return { | |
| originalText: text, | |
| transformedText: text, | |
| triggersFound: [], | |
| techniqueUsed: config.technique, | |
| transformations: [] | |
| } | |
| } | |
| const triggersFound = detectTriggers(text, config.customTriggers) | |
| if (triggersFound.length === 0) { | |
| return { | |
| originalText: text, | |
| transformedText: text, | |
| triggersFound: [], | |
| techniqueUsed: config.technique, | |
| transformations: [] | |
| } | |
| } | |
| let transformedText = text | |
| const transformations: ParseltongueResult['transformations'] = [] | |
| // Sort triggers by length (longest first) to avoid partial replacements | |
| const sortedTriggers = [...triggersFound].sort((a, b) => b.length - a.length) | |
| for (const trigger of sortedTriggers) { | |
| const regex = new RegExp(`\\b(${trigger.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})\\b`, 'gi') | |
| transformedText = transformedText.replace(regex, (match) => { | |
| const transformed = obfuscateWord(match, config.technique, config.intensity) | |
| transformations.push({ | |
| original: match, | |
| transformed, | |
| technique: config.technique | |
| }) | |
| return transformed | |
| }) | |
| } | |
| return { | |
| originalText: text, | |
| transformedText, | |
| triggersFound, | |
| techniqueUsed: config.technique, | |
| transformations | |
| } | |
| } | |
| /** | |
| * Get the default parseltongue config | |
| */ | |
| export function getDefaultConfig(): ParseltongueConfig { | |
| return { | |
| enabled: false, | |
| technique: 'leetspeak', | |
| intensity: 'medium', | |
| customTriggers: [] | |
| } | |
| } | |
| /** | |
| * Get human-readable description of a technique | |
| */ | |
| export function getTechniqueDescription(technique: ObfuscationTechnique): string { | |
| const descriptions: Record<ObfuscationTechnique, string> = { | |
| leetspeak: 'Classic 1337speak: a→4, e→3, etc.', | |
| unicode: 'Unicode lookalikes (cyrillic, greek)', | |
| zwj: 'Invisible zero-width characters', | |
| mixedcase: 'Disrupted casing patterns', | |
| phonetic: 'Phonetic spelling substitutions', | |
| random: 'Random mix of all techniques' | |
| } | |
| return descriptions[technique] | |
| } | |
| /** | |
| * Get available techniques for UI | |
| */ | |
| export function getAvailableTechniques(): Array<{ | |
| id: ObfuscationTechnique | |
| name: string | |
| description: string | |
| }> { | |
| return [ | |
| { id: 'leetspeak', name: 'L33tspeak', description: getTechniqueDescription('leetspeak') }, | |
| { id: 'unicode', name: 'Unicode', description: getTechniqueDescription('unicode') }, | |
| { id: 'zwj', name: 'Zero-Width', description: getTechniqueDescription('zwj') }, | |
| { id: 'mixedcase', name: 'MiXeD CaSe', description: getTechniqueDescription('mixedcase') }, | |
| { id: 'phonetic', name: 'Phonetic', description: getTechniqueDescription('phonetic') }, | |
| { id: 'random', name: 'Random', description: getTechniqueDescription('random') } | |
| ] | |
| } | |
| /** | |
| * Export the default trigger list for reference | |
| */ | |
| export { DEFAULT_TRIGGERS } | |