UNPKG

@kanadi/core

Version:

Multi-Layer CAPTCHA Framework with customizable validators and challenge bundles

281 lines (242 loc) 7.78 kB
import type { IChallengeValidator } from "../middleware/types"; import { Bundle } from "./bundle"; import type { ChallengeResponse } from "./types"; import { ValidatorSessionRepository } from "../models/validator-session.repository"; import { BundleSessionRepository } from "../models/bundle-session.repository"; import { CryptoUtil } from "./crypto.util"; import { createLogger } from "../utils/logger"; export class KanadiContext { private sessionId: string; private bundles: Map<string, Bundle> = new Map(); private validators: Map<string, IChallengeValidator>; public metadata: Record<string, any> = {}; public userAgent?: string; public ip?: string; public referrer?: string; private pendingValidators: any[] = []; private currentValidatorIndex: number = 0; private validatorSessionRepo: ValidatorSessionRepository; private bundleSessionRepo: BundleSessionRepository; private currentBundleSessionId?: string; private readonly logger = createLogger(KanadiContext.name); constructor( sessionId: string, validators: Map<string, IChallengeValidator>, metadata?: Record<string, any>, ) { this.sessionId = sessionId; this.validators = validators; this.validatorSessionRepo = new ValidatorSessionRepository(); this.bundleSessionRepo = new BundleSessionRepository(); if (metadata) { this.metadata = metadata; } } async createBundle(bundleId: string): Promise<Bundle> { const bundle = new Bundle(bundleId); this.bundles.set(bundleId, bundle); return bundle; } getBundle(bundleId: string): Bundle | undefined { return this.bundles.get(bundleId); } async challenge(options: { bundleId: Bundle | string; timeout: number; }): Promise<ChallengeResponse> { const bundleId = typeof options.bundleId === "string" ? options.bundleId : options.bundleId.id; const bundle = this.bundles.get(bundleId); if (!bundle) { throw new Error(`Bundle ${bundleId} not found`); } const challenges: any[] = []; const validatorIds: string[] = []; for (const [validatorSessionId, sessionData] of bundle.getValidators()) { const validator = this.validators.get(sessionData.validatorId); if (validator) { bundle.registerValidator(validatorSessionId, validator); this.metadata.validatorConfig = sessionData.data; const contextWithConfig = { userAgent: this.userAgent, ip: this.ip, referrer: this.referrer, trustScore: this.metadata.trustScore || 50, metadata: this.metadata, }; const challengeStep = validator.generateChallenge(contextWithConfig); challenges.push(challengeStep); validatorIds.push(sessionData.validatorId); } } try { const bundleSession = await this.bundleSessionRepo.create({ bundleId, sessionId: this.sessionId, userId: this.metadata.userId, deviceId: this.metadata.deviceId, validatorIds, ip: this.ip, userAgent: this.userAgent, referrer: this.referrer, metadata: { trustScore: this.metadata.trustScore || 50, }, }); this.currentBundleSessionId = bundleSession.id; this.logger.log(`Bundle session created: ${this.currentBundleSessionId}`); } catch (error) { this.logger.error("Failed to create bundle session", error); } this.pendingValidators = challenges; this.currentValidatorIndex = 0; const challengeId = this.generateChallengeId(); const firstValidator = this.pendingValidators[0]; if (!firstValidator) { throw new Error("No validators in bundle"); } return { status: "Challenge created successfully", sessionId: this.sessionId, challenge: { id: challengeId, solveId: firstValidator.solveId, validatorId: firstValidator.validatorId, data: firstValidator.data, timeout: firstValidator.timeout, }, hasMore: this.pendingValidators.length > 1, timeout: options.timeout, }; } async validate(solveId: string, response: any): Promise<any> { const currentValidator = this.pendingValidators[this.currentValidatorIndex]; if (!currentValidator || currentValidator.solveId !== solveId) { return { status: "error", message: "Invalid or expired challenge", }; } const validator = this.validators.get(currentValidator.validatorId); if (!validator) { return { status: "error", message: "Validator not found", }; } let decryptedResponse = response; let encryptedData: any = null; if (currentValidator.encrypted && Array.isArray(response)) { const encryptionKey = this.metadata.solveIdKeys?.[solveId]; if (!encryptionKey) { this.logger.error(`Encryption key not found for solveId: ${solveId}`); return { status: "error", message: "Encryption key not found", }; } try { encryptedData = response; decryptedResponse = CryptoUtil.decryptFromArray(response, encryptionKey); this.logger.log(`Response decrypted at Context level for solveId: ${solveId}`); } catch (error) { this.logger.error("Decryption failed at Context level", error); return { status: "error", message: "Failed to decrypt response", error: error instanceof Error ? error.message : "Unknown error", }; } } const validationResult = await validator.validate( { sessionId: this.sessionId, userAgent: this.userAgent, ip: this.ip, referrer: this.referrer, trustScore: this.metadata.trustScore || 50, metadata: this.metadata, }, solveId, decryptedResponse, ); try { await this.validatorSessionRepo.create({ bundleSessionId: this.currentBundleSessionId, validatorId: currentValidator.validatorId, sessionId: this.sessionId, solveId, result: validationResult.result, score: validationResult.score, confidence: validationResult.confidence, processingTime: validationResult.processingTime, evidence: validationResult.evidence, decryptedData: decryptedResponse, }); this.logger.log("Validator session saved to database"); } catch (error) { this.logger.error("Failed to save validator session", error); } if (validationResult.result !== "PASS") { return { status: "error", message: "Validation failed", details: validationResult, }; } this.currentValidatorIndex++; if (this.currentValidatorIndex < this.pendingValidators.length) { const nextValidator = this.pendingValidators[this.currentValidatorIndex]; return { status: "next", challenge: { id: this.generateChallengeId(), solveId: nextValidator.solveId, validatorId: nextValidator.validatorId, data: nextValidator.data, timeout: nextValidator.timeout, }, hasMore: this.currentValidatorIndex < this.pendingValidators.length - 1, }; } if (this.currentBundleSessionId) { try { await this.bundleSessionRepo.update(this.currentBundleSessionId, { result: "PASS", totalScore: 100, confidence: 0.95, completedAt: new Date(), }); this.logger.log("Bundle session updated: PASS"); } catch (error) { this.logger.error("Failed to update bundle session", error); } } return { status: "success", message: "All challenges completed successfully", }; } getSessionId(): string { return this.sessionId; } getClientIp(): string | undefined { return this.ip; } getUserAgent(): string | undefined { return this.userAgent; } getReferrer(): string | undefined { return this.referrer; } setClientInfo(info: { ip?: string; userAgent?: string; referrer?: string }): void { if (info.ip) this.ip = info.ip; if (info.userAgent) this.userAgent = info.userAgent; if (info.referrer) this.referrer = info.referrer; } private generateChallengeId(): string { return `ch_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; } }