@baanihali/captcha
Version:
A customizable sliding puzzle captcha component for React applications with server-side validation
155 lines (154 loc) • 4.6 kB
JavaScript
// 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
*/