@kanadi/core
Version:
Multi-Layer CAPTCHA Framework with customizable validators and challenge bundles
173 lines (152 loc) • 4 kB
text/typescript
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";
()
({
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(),
};
}
}