@kanadi/core
Version:
Multi-Layer CAPTCHA Framework with customizable validators and challenge bundles
203 lines (172 loc) • 4.97 kB
text/typescript
import { ForbiddenException, Injectable } from "@nestjs/common";
import type { DatabaseConfig } from "../database/types";
import { KanadiClient } from "./client";
import { KanadiContext } from "./context";
import type {
ChallengeResponse,
SiteVerifyRequest,
SiteVerifyResponse,
} from "./types";
import { BanRuleEngine } from "../ban/ban-engine.service";
import type { BanContext, BanCheckResult } from "../ban/types";
import { createLogger } from "../utils/logger";
()
export class KanadiGatewayService {
private readonly logger = createLogger(KanadiGatewayService.name);
private sessions: Map<string, KanadiContext> = new Map();
private gatewayHandler?: any;
private postgresDB: any = null;
private redisClient: any = null;
private banEngine: BanRuleEngine;
constructor(private readonly client: KanadiClient) {
this.banEngine = new BanRuleEngine();
this.initializeDatabases();
}
private async initializeDatabases() {
const configs = this.client.getDatabaseConfigs();
for (const config of configs) {
if (config.type === "psql") {
try {
const options = config.options as any;
this.postgresDB = Bun.sql(options.url);
this.logger.log("PostgreSQL connected");
} catch (error: any) {
this.logger.error(`PostgreSQL connection failed: ${error.message}`);
}
} else if (config.type === "redis") {
try {
const options = config.options as any;
this.logger.log("Redis connection pending (Bun.redis API not stable)");
} catch (error: any) {
this.logger.error(`Redis connection failed: ${error.message}`);
}
}
}
}
async checkDatabaseStatus() {
const status = {
postgres: { connected: false, error: null as string | null },
redis: { connected: false, error: null as string | null },
};
try {
if (this.postgresDB) {
await this.postgresDB`SELECT 1 as test`;
status.postgres.connected = true;
}
} catch (error: any) {
status.postgres.error = error.message;
}
try {
if (this.redisClient) {
await this.redisClient.ping();
status.redis.connected = true;
}
} catch (error: any) {
status.redis.error = error.message;
}
return status;
}
async createChallenge(
userAgent?: string,
ip?: string,
referrer?: string,
headers?: Record<string, string | string[] | undefined>,
clientFingerprint?: string,
): Promise<ChallengeResponse> {
const sessionId = this.generateSessionId();
const context = this.client.createContext(sessionId, {
userAgent,
ip,
referrer,
trustScore: 50,
clientFingerprint,
headers,
});
context.userAgent = userAgent;
context.ip = ip;
context.referrer = referrer;
context.metadata.clientFingerprint = clientFingerprint;
context.metadata.headers = headers;
const banContext: BanContext = {
ip,
fingerprint: clientFingerprint,
sessionId,
userAgent,
timestamp: new Date(),
metadata: {},
};
const banResult = await this.banEngine.checkBan(banContext);
if (banResult) {
this.logger.warn(
`Ban detected: ${banResult.reason} (rule: ${banResult.ruleName})`,
);
const reasonId = banResult.reasonId ?? banResult.ruleId;
throw new ForbiddenException({
reasonId,
ruleId: banResult.ruleId,
});
}
this.sessions.set(sessionId, context);
if (this.gatewayHandler?.handleOnChallengeCreated) {
return await this.gatewayHandler.handleOnChallengeCreated(context);
}
return {
status: "Challenge created successfully",
sessionId,
challenge: {
id: this.generateChallengeId(),
},
timeout: 10000,
};
}
async verifyChallenge(
request: SiteVerifyRequest,
): Promise<SiteVerifyResponse> {
const { sessionId, challengeId, response } = request;
const context = this.sessions.get(sessionId);
if (!context) {
return {
status: "error",
message: "Session not found or expired",
};
}
const solveId = (request as any).solveId;
if (!solveId) {
return {
status: "error",
message: "Missing solveId",
};
}
if (this.gatewayHandler?.handleOnChallengeValidated) {
const result = await this.gatewayHandler.handleOnChallengeValidated(
context,
solveId,
response,
);
if (result.status === "success") {
this.sessions.delete(sessionId);
}
return result;
}
const result = await context.validate(solveId, response);
if (result.status === "success") {
this.sessions.delete(sessionId);
}
return result;
}
setGatewayHandler(handler: any): void {
(this as any).gatewayHandler = handler;
}
getBanEngine(): BanRuleEngine {
return this.banEngine;
}
async checkBan(context: BanContext): Promise<BanCheckResult | null> {
return await this.banEngine.checkBan(context);
}
private generateSessionId(): string {
return `sid_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
}
private generateChallengeId(): string {
return `ch_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
}
}