import type { CaptchaConcreteImageStyle, CaptchaImageStyle } from "../types.ts"; export const CAPTCHA_CONCRETE_IMAGE_STYLES = [ "classic", "collision", "snow", "corrosion", "spiderweb", "cross-shadow", "split", "split2", "cut", "darts", "distortion", "stitch", "striped", "wave", "grid-noise", "scribble", "pixel", "broken-lines", ] as const satisfies readonly CaptchaConcreteImageStyle[]; export const CAPTCHA_IMAGE_STYLES = [ "random", ...CAPTCHA_CONCRETE_IMAGE_STYLES, ] as const satisfies readonly CaptchaImageStyle[]; const CONCRETE_STYLE_SET = new Set(CAPTCHA_CONCRETE_IMAGE_STYLES); const STYLE_SET = new Set(CAPTCHA_IMAGE_STYLES); function styleValues(value: unknown): string[] { if (Array.isArray(value)) return value.flatMap((item) => styleValues(item)); if (typeof value !== "string") return []; return value .split(",") .map((item) => item.trim().toLowerCase()) .filter(Boolean); } export function isCaptchaImageStyle(value: unknown): value is CaptchaImageStyle { return typeof value === "string" && STYLE_SET.has(value.trim().toLowerCase()); } export function normalizeCaptchaImageStyle( value: unknown, fallback: CaptchaImageStyle = "random", ): CaptchaImageStyle { if (value === undefined || value === null || value === "") return fallback; const normalized = String(value).trim().toLowerCase(); if (!STYLE_SET.has(normalized)) { throw new RangeError(`imageStyle must be one of: ${CAPTCHA_IMAGE_STYLES.join(", ")}`); } return normalized as CaptchaImageStyle; } export function normalizeCaptchaImageStyleList( value: unknown, name: "allowedStyles" | "excludedStyles", ): CaptchaConcreteImageStyle[] { const styles: CaptchaConcreteImageStyle[] = []; for (const entry of styleValues(value)) { if (entry === "random") continue; if (!CONCRETE_STYLE_SET.has(entry)) { throw new RangeError(`${name} contains an unknown image style: ${entry}`); } const style = entry as CaptchaConcreteImageStyle; if (!styles.includes(style)) styles.push(style); } return styles; } export interface ResolveCaptchaImageStyleOptions { imageStyle?: unknown; allowedStyles?: unknown; excludedStyles?: unknown; randomizeStyle?: boolean; randomInt(min: number, max: number): number; } export interface ResolvedCaptchaImageStyle { requested: CaptchaImageStyle; resolved: CaptchaConcreteImageStyle; pool: CaptchaConcreteImageStyle[]; } export function resolveCaptchaImageStyle( options: ResolveCaptchaImageStyleOptions, ): ResolvedCaptchaImageStyle { const requested = normalizeCaptchaImageStyle(options.imageStyle, "random"); const allowed = normalizeCaptchaImageStyleList(options.allowedStyles, "allowedStyles"); const excluded = new Set( normalizeCaptchaImageStyleList(options.excludedStyles, "excludedStyles"), ); const source = allowed.length ? allowed : [...CAPTCHA_CONCRETE_IMAGE_STYLES]; const pool = source.filter((style) => !excluded.has(style)); if (!pool.length) { throw new RangeError( "No CAPTCHA image styles remain after applying allowedStyles and excludedStyles", ); } if (!options.randomizeStyle && requested !== "random") { if (!pool.includes(requested)) { throw new RangeError(`imageStyle ${requested} is not available in the configured style pool`); } return { requested, resolved: requested, pool }; } return { requested, resolved: pool[options.randomInt(0, pool.length - 1)]!, pool, }; }