118 lines
3.4 KiB
TypeScript
118 lines
3.4 KiB
TypeScript
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<string>(CAPTCHA_CONCRETE_IMAGE_STYLES);
|
|
const STYLE_SET = new Set<string>(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,
|
|
};
|
|
}
|