UNPKG

@kanadi/core

Version:

Multi-Layer CAPTCHA Framework with customizable validators and challenge bundles

173 lines (152 loc) 4 kB
import { Injectable } from "@nestjs/common"; import { createHash, randomBytes } from "crypto"; import { type ChallengeContext, ChallengeResult, type ChallengeStep, type IChallengeValidator, type StepValidationResult, Validator, type ValidatorMetadata, } from "../middleware"; @Injectable() @Validator({ id: "pow_v2", name: "Proof of Work V2", priority: 5, required: true, category: "computational", description: "Computational proof of work challenge to prevent automated abuse", }) export class PowV2Validator implements IChallengeValidator { private metadata: ValidatorMetadata; constructor() { this.metadata = Reflect.getMetadata("middleware:metadata", PowV2Validator); } generateChallenge(context: { userAgent: string | undefined; ip: string | undefined; referrer: string | undefined; trustScore: any; metadata: Record<string, any>; }): ChallengeStep { const solveId = this.generateSolveId(); const difficulty = context.metadata.validatorConfig?.difficulty ?? this.calculateDifficulty(context); const challenge = this.generatePowChallenge(difficulty); if (!context.metadata.powChallenges) { context.metadata.powChallenges = {}; } context.metadata.powChallenges[solveId] = { challenge: challenge.challenge, prefix: challenge.prefix, difficulty, issuedAt: Date.now(), }; return { solveId, validatorId: this.metadata.id, data: { challenge: challenge.challenge, difficulty, algorithm: "sha256", prefix: challenge.prefix, }, timeout: 30, }; } async validate( context: ChallengeContext, solveId: string, submittedData: any, ): Promise<StepValidationResult> { const startTime = Date.now(); const storedChallenge = context.metadata.powChallenges?.[solveId]; if (!storedChallenge) { return this.createResult(solveId, startTime, ChallengeResult.FAIL, 0, { reason: "Challenge not found", }); } const { nonce } = submittedData; if (typeof nonce !== "number") { return this.createResult(solveId, startTime, ChallengeResult.FAIL, 0, { reason: "Invalid nonce format", }); } const responseTime = Date.now() - storedChallenge.issuedAt; if (responseTime > 30000) { return this.createResult(solveId, startTime, ChallengeResult.FAIL, 0, { reason: "Response timeout", responseTime, }); } const hash = this.computeHash(storedChallenge.challenge, nonce); delete context.metadata.powChallenges[solveId]; return this.createResult( solveId, startTime, ChallengeResult.PASS, 100, { hash, nonce, responseTime, difficulty: storedChallenge.difficulty, }, 0.95, ); } canExecute(context: ChallengeContext): boolean { return true; } calculateDifficulty(context: ChallengeContext): number { if (context.trustScore < 30) { return 6; } else if (context.trustScore < 60) { return 5; } else if (context.trustScore < 80) { return 4; } else { return 3; // Easier for high trust } } private generatePowChallenge(difficulty: number): { challenge: string; prefix: string; } { const challenge = randomBytes(16).toString("hex"); const prefix = "0".repeat(difficulty); return { challenge, prefix }; } private computeHash(challenge: string, nonce: number): string { const data = `${challenge}${nonce}`; return createHash("sha256").update(data).digest("hex"); } private generateSolveId(): string { return randomBytes(8).toString("hex").toUpperCase(); } private createResult( solveId: string, startTime: number, result: ChallengeResult, score: number, evidence: Record<string, any>, confidence: number = 0.9, ): StepValidationResult { return { solveId, validatorId: this.metadata.id, result, score, confidence, processingTime: Date.now() - startTime, evidence: result === ChallengeResult.FAIL ? { reason: "Validation failed" } : evidence, // Don't expose detailed error info timestamp: new Date(), }; } }