2026-03-07 08:52:24 +00:00
|
|
|
// Bot customization validation
|
|
|
|
|
// All values are validated against whitelists to prevent code injection
|
|
|
|
|
|
|
|
|
|
const VALID_ARCHETYPES = new Set([
|
|
|
|
|
'standard', 'lobster', 'sheep', 'cyborg', 'blob', 'tank', 'dog', 'cat',
|
|
|
|
|
'cactus', 'pizza', 'mushroom', 'shark', 'penguin', 'octopus', 'skeleton',
|
|
|
|
|
'ghost', 'alien', 'dinosaur', 'pirate', 'ninja', 'cowboy', 'wizard',
|
|
|
|
|
'bee', 'frog', 'snail', 'robot', 'android', 'drone', 'toaster', 'tv_head',
|
|
|
|
|
'calculator', 'satellite', 'mech', 'led_cube', 'circuit', 'antenna_bot',
|
|
|
|
|
'microwave', 'cyberdog', 'robocat', 'ufo_bot', 'minotaur', 'unicorn',
|
|
|
|
|
'phoenix', 'dragon', 'mermaid', 'griffin', 'cyclops', 'gargoyle', 'golem',
|
|
|
|
|
'vampire', 'werewolf', 'zombie', 'witch', 'demon', 'chef', 'firefighter',
|
|
|
|
|
'astronaut', 'clown', 'detective', 'nurse', 'lumberjack', 'scientist',
|
|
|
|
|
'wrestler', 'boxer', 'gladiator', 'samurai', 'viking', 'knight',
|
|
|
|
|
'elephant', 'giraffe', 'hippo', 'lion', 'monkey', 'parrot', 'raccoon',
|
|
|
|
|
'snake', 'turtle', 'whale', 'crocodile', 'flamingo', 'hedgehog', 'panda',
|
|
|
|
|
'hamster', 'sock_puppet', 'traffic_cone', 'toilet_man', 'potato',
|
|
|
|
|
'cloud_man', 'rock_man', 'balloon_man', 'trash_can', 'rubber_duck',
|
|
|
|
|
'snowman', 'scarecrow', 'jack_o_lantern', 'garden_gnome', 'lamp_post',
|
2026-03-08 12:08:18 +00:00
|
|
|
'broom_man', 'human', 'the_creator',
|
2026-03-07 08:52:24 +00:00
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/
|
|
|
|
|
const HSL_COLOR_RE = /^hsl\(\d{1,3},\s?\d{1,3}%,\s?\d{1,3}%\)$/
|
|
|
|
|
|
|
|
|
|
export interface BotCustomization {
|
|
|
|
|
archetype?: string
|
|
|
|
|
primaryColor?: string
|
|
|
|
|
secondaryColor?: string
|
|
|
|
|
forceVisor?: boolean
|
|
|
|
|
forceMohawk?: boolean
|
|
|
|
|
forceHorns?: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isValidColor(c: string): boolean {
|
|
|
|
|
return HEX_COLOR_RE.test(c) || HSL_COLOR_RE.test(c)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hexToHsl(hex: string): string {
|
|
|
|
|
const r = parseInt(hex.slice(1, 3), 16) / 255
|
|
|
|
|
const g = parseInt(hex.slice(3, 5), 16) / 255
|
|
|
|
|
const b = parseInt(hex.slice(5, 7), 16) / 255
|
|
|
|
|
const max = Math.max(r, g, b), min = Math.min(r, g, b)
|
|
|
|
|
const l = (max + min) / 2
|
|
|
|
|
if (max === min) return `hsl(0, 0%, ${Math.round(l * 100)}%)`
|
|
|
|
|
const d = max - min
|
|
|
|
|
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
|
|
|
|
let h = 0
|
|
|
|
|
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6
|
|
|
|
|
else if (max === g) h = ((b - r) / d + 2) / 6
|
|
|
|
|
else h = ((r - g) / d + 4) / 6
|
|
|
|
|
return `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function validateCustomization(raw: unknown): { valid: true; data: BotCustomization } | { valid: false; error: string } {
|
|
|
|
|
if (raw == null) return { valid: true, data: {} }
|
|
|
|
|
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
|
|
|
return { valid: false, error: 'Customization must be an object.' }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const obj = raw as Record<string, unknown>
|
|
|
|
|
const result: BotCustomization = {}
|
|
|
|
|
|
|
|
|
|
if (obj.archetype !== undefined) {
|
|
|
|
|
if (typeof obj.archetype !== 'string' || !VALID_ARCHETYPES.has(obj.archetype)) {
|
|
|
|
|
return { valid: false, error: `Invalid archetype. Must be one of: ${[...VALID_ARCHETYPES].slice(0, 10).join(', ')}...` }
|
|
|
|
|
}
|
|
|
|
|
result.archetype = obj.archetype
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (obj.primaryColor !== undefined) {
|
|
|
|
|
if (typeof obj.primaryColor !== 'string' || !isValidColor(obj.primaryColor)) {
|
|
|
|
|
return { valid: false, error: 'primaryColor must be a valid hex (#RRGGBB) or hsl color.' }
|
|
|
|
|
}
|
|
|
|
|
result.primaryColor = HEX_COLOR_RE.test(obj.primaryColor) ? hexToHsl(obj.primaryColor) : obj.primaryColor
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (obj.secondaryColor !== undefined) {
|
|
|
|
|
if (typeof obj.secondaryColor !== 'string' || !isValidColor(obj.secondaryColor)) {
|
|
|
|
|
return { valid: false, error: 'secondaryColor must be a valid hex (#RRGGBB) or hsl color.' }
|
|
|
|
|
}
|
|
|
|
|
result.secondaryColor = HEX_COLOR_RE.test(obj.secondaryColor) ? hexToHsl(obj.secondaryColor) : obj.secondaryColor
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (obj.forceVisor !== undefined) {
|
|
|
|
|
if (typeof obj.forceVisor !== 'boolean') return { valid: false, error: 'forceVisor must be a boolean.' }
|
|
|
|
|
result.forceVisor = obj.forceVisor
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (obj.forceMohawk !== undefined) {
|
|
|
|
|
if (typeof obj.forceMohawk !== 'boolean') return { valid: false, error: 'forceMohawk must be a boolean.' }
|
|
|
|
|
result.forceMohawk = obj.forceMohawk
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (obj.forceHorns !== undefined) {
|
|
|
|
|
if (typeof obj.forceHorns !== 'boolean') return { valid: false, error: 'forceHorns must be a boolean.' }
|
|
|
|
|
result.forceHorns = obj.forceHorns
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reject any unexpected keys
|
|
|
|
|
const allowedKeys = new Set(['archetype', 'primaryColor', 'secondaryColor', 'forceVisor', 'forceMohawk', 'forceHorns'])
|
|
|
|
|
for (const key of Object.keys(obj)) {
|
|
|
|
|
if (!allowedKeys.has(key)) {
|
|
|
|
|
return { valid: false, error: `Unknown customization key: ${key}` }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { valid: true, data: result }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getArchetypeList(): string[] {
|
|
|
|
|
return [...VALID_ARCHETYPES].sort()
|
|
|
|
|
}
|