@kanadi/core
Version:
Multi-Layer CAPTCHA Framework with customizable validators and challenge bundles
281 lines (242 loc) • 7.78 kB
text/typescript
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)}`;
}
}