UNPKG

@baanihali/captcha

Version:

A customizable sliding puzzle captcha component for React applications with server-side validation

155 lines (154 loc) 4.6 kB
// src/server/index.ts import "server-only"; import { randomUUID } from "crypto"; import sharp from "sharp"; import fs from "fs"; var ON_VERIFY_CAPTCHA_VALUE = "verified"; var createCaptcha = async ({ image, captchaId, fallbackImgPath, storeCaptchaId }) => { const sliderOffset = Math.max(55, Math.floor(Math.random() * 249)); const _captchaId = captchaId || randomUUID(); const _image = image || await getCaptchaImage(fallbackImgPath); if (!_image) { throw new Error("Failed to obtain captcha image"); } const originalImage = sharp(_image); const [puzzlePieceBuffer, composedImage, _] = await Promise.all([ // Extract the puzzle piece from the original image originalImage.clone().extract({ left: sliderOffset, top: 75, width: 50, height: 50 }).jpeg({ quality: 30 }).toBuffer(), // Create background with puzzle piece area blacked out and blurred originalImage.composite([ { input: await sharp({ create: { width: 50, height: 50, channels: 3, background: { r: 0, g: 0, b: 0 } } }).jpeg({ quality: 50 }).toBuffer(), top: 75, left: sliderOffset, blend: "over" } ]).blur().jpeg({ quality: 30 }).toBuffer(), // Store the captcha solution in your database/redis/etc. storeCaptchaId(_captchaId, sliderOffset.toString()) ]); return { puzzle: `data:image/jpeg;base64,${puzzlePieceBuffer.toString("base64")}`, background: `data:image/jpeg;base64,${composedImage.toString("base64")}`, id: _captchaId }; }; var getCaptchaImage = async (fallbackImgPath) => { try { const imageId = Math.floor(Math.random() * 500); const imageUrl = `https://picsum.photos/id/${imageId}/300/200`; const response = await fetch(imageUrl); if (response.ok) { return await response.arrayBuffer(); } if (fallbackImgPath && fs.existsSync(fallbackImgPath)) { return fs.readFileSync(fallbackImgPath); } return null; } catch (error) { if (fallbackImgPath && fs.existsSync(fallbackImgPath)) { try { return fs.readFileSync(fallbackImgPath); } catch (fallbackError) { throw new Error( `Failed to fetch image from API and fallback: ${fallbackError instanceof Error ? fallbackError?.message : "Unknown error"}` ); } } throw new Error( `Failed to fetch captcha image: ${error instanceof Error ? error?.message : "Unknown error"}` ); } }; var verifyCaptcha = async (props) => { const { captchaId, value, getCaptchaValue, changeCaptchaIdOnSuccess, tolerance } = props; try { const storedValue = await getCaptchaValue(captchaId); if (!storedValue) { return { success: false, reason: "Captcha not found or has expired" }; } if (storedValue === ON_VERIFY_CAPTCHA_VALUE) { return { success: true }; } const userPosition = parseInt(value, 10); const correctPosition = parseInt(storedValue, 10); if (isNaN(userPosition) || isNaN(correctPosition)) { return { success: false, reason: "Invalid position values" }; } const _tolerance = tolerance || 10; const isCorrect = Math.abs(correctPosition - userPosition) <= _tolerance; if (isCorrect) { await changeCaptchaIdOnSuccess(captchaId, ON_VERIFY_CAPTCHA_VALUE); return { success: true }; } return { success: false, reason: "Please position the puzzle piece correctly" }; } catch (error) { return { success: false, reason: `Verification error: ${error instanceof Error ? error.message : "Unknown error"}` }; } }; var hasCaptchaBeenVerified = async (props) => { const value = await props.getCaptchaValue(props.captchaId); return value === ON_VERIFY_CAPTCHA_VALUE; }; export { ON_VERIFY_CAPTCHA_VALUE, createCaptcha, getCaptchaImage, hasCaptchaBeenVerified, verifyCaptcha }; /** * Server-side Captcha Validation Package * * This module provides server-side functions for creating and validating * sliding puzzle captchas. It includes image processing, random puzzle * generation, and secure verification with replay attack prevention. * * Key Features: * - Random puzzle piece positioning * - Base64 image encoding for easy client transmission * - Configurable tolerance for user positioning * - Fallback image support * - Comprehensive error handling * * @packageDocumentation * @author Murtaza Baanihali * @license MIT */