import { createHash } from 'crypto' import { createCanvas } from 'canvas' export const CAPTCHA_LENGTH = 6 const CAPTCHA_CHARS = 'ABCDEFGHIJKMNOPRSTUVWXYZ123456789' // Notice that ambiguous characters are removed /** Hash a captcha value */ function hashCaptchaValue(value: string): string { return createHash('sha256').update(value).digest('hex') } /** Generate a captcha image as a data URI */ export function generateCaptchaImage(text: string) { const width = 144 const height = 48 const canvas = createCanvas(width, height) const ctx = canvas.getContext('2d') // Fill background with gradient const gradient = ctx.createLinearGradient(0, 0, width, height) gradient.addColorStop(0, '#1a202c') gradient.addColorStop(1, '#2d3748') ctx.fillStyle = gradient ctx.fillRect(0, 0, width, height) // Add grid pattern background ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)' ctx.lineWidth = 1 for (let i = 0; i < width; i += 10) { ctx.beginPath() ctx.moveTo(i, 0) ctx.lineTo(i, height) ctx.stroke() } for (let i = 0; i < height; i += 10) { ctx.beginPath() ctx.moveTo(0, i) ctx.lineTo(width, i) ctx.stroke() } // Add wavy lines for (let i = 0; i < 3; i++) { const r = Math.floor(Math.random() * 200 + 55).toString() const g = Math.floor(Math.random() * 200 + 55).toString() const b = Math.floor(Math.random() * 200 + 55).toString() ctx.strokeStyle = 'rgba(' + r + ', ' + g + ', ' + b + ', 0.5)' ctx.lineWidth = Math.random() * 3 + 1 ctx.beginPath() const startY = Math.random() * height let curveX = 0 let curveY = startY ctx.moveTo(curveX, curveY) while (curveX < width) { const nextX = curveX + Math.random() * 20 + 10 const nextY = startY + Math.sin(curveX / 20) * (Math.random() * 15 + 5) ctx.quadraticCurveTo(curveX + (nextX - curveX) / 2, curveY + Math.random() * 20 - 10, nextX, nextY) curveX = nextX curveY = nextY } ctx.stroke() } // Add random dots for (let i = 0; i < 100; i++) { const r = Math.floor(Math.random() * 200 + 55).toString() const g = Math.floor(Math.random() * 200 + 55).toString() const b = Math.floor(Math.random() * 200 + 55).toString() const alpha = (Math.random() * 0.5 + 0.1).toString() ctx.fillStyle = 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')' ctx.beginPath() ctx.arc(Math.random() * width, Math.random() * height, Math.random() * 2 + 1, 0, Math.PI * 2) ctx.fill() } // Draw captcha text with shadow and more distortion ctx.shadowColor = 'rgba(0, 0, 0, 0.5)' ctx.shadowBlur = 3 ctx.shadowOffsetX = 2 ctx.shadowOffsetY = 2 // Calculate text positioning const fontSize = Math.floor(height * 0.55) const padding = 15 const charWidth = (width - padding * 2) / text.length // Draw each character with various effects for (let i = 0; i < text.length; i++) { const r = Math.floor(Math.random() * 100 + 155).toString() const g = Math.floor(Math.random() * 100 + 155).toString() const b = Math.floor(Math.random() * 100 + 155).toString() ctx.fillStyle = 'rgb(' + r + ', ' + g + ', ' + b + ')' // Vary font style for each character const fontStyle = Math.random() > 0.5 ? 'bold' : 'normal' const fontFamily = Math.random() > 0.5 ? 'monospace' : 'Arial' const fontSizeStr = fontSize.toString() ctx.font = fontStyle + ' ' + fontSizeStr + 'px ' + fontFamily // Position with variations const xPos = padding + i * charWidth + (Math.random() * 8 - 4) const yPos = height * 0.6 + (Math.random() * 12 - 6) const rotation = Math.random() * 0.6 - 0.3 const scale = 0.8 + Math.random() * 0.4 ctx.save() ctx.translate(xPos, yPos) ctx.rotate(rotation) ctx.scale(scale, scale) // Add slight perspective effect if (Math.random() > 0.7) { ctx.transform(1, 0, 0.1, 1, 0, 0) } else if (Math.random() > 0.5) { ctx.transform(1, 0, -0.1, 1, 0, 0) } ctx.fillText(text.charAt(i), 0, 0) ctx.restore() } // Add some noise on top for (let x = 0; x < width; x += 3) { for (let y = 0; y < height; y += 3) { if (Math.random() > 0.95) { const alpha = (Math.random() * 0.2 + 0.1).toString() ctx.fillStyle = 'rgba(255, 255, 255, ' + alpha + ')' ctx.fillRect(x, y, 2, 2) } } } return { src: canvas.toDataURL('image/png'), width, height, format: 'png', } as const satisfies ImageMetadata } /** Generate a new captcha with solution, its hash, and image data URI */ export function generateCaptcha() { const solution = Array.from({ length: CAPTCHA_LENGTH }, () => CAPTCHA_CHARS.charAt(Math.floor(Math.random() * CAPTCHA_CHARS.length)) ).join('') return { solution, solutionHash: hashCaptchaValue(solution), image: generateCaptchaImage(solution), } } /** Verify a captcha input against the expected hash */ export function verifyCaptcha(value: string, solutionHash: string): boolean { const correctedValue = value .toUpperCase() .replace(/[^A-Z0-9]/g, '') .replace(/0/g, 'O') .replace(/Q/g, 'O') .replace(/L/g, 'I') const valueHash = hashCaptchaValue(correctedValue) return valueHash === solutionHash }