UNPKG

@kanadi/core

Version:

Multi-Layer CAPTCHA Framework with customizable validators and challenge bundles

203 lines (172 loc) 4.97 kB
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"; @Injectable() 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)}`; } }