UNPKG

@kanadi/core

Version:

Multi-Layer CAPTCHA Framework with customizable validators and challenge bundles

232 lines (215 loc) 5.76 kB
import { sql } from "bun"; export interface KanadiBundleSession { id: string; bundle_id: string; session_id?: string; user_id?: string; device_id?: string; validator_ids?: string[]; ip?: string; user_agent?: string; referrer?: string; result?: string; total_score: number; confidence?: number; metadata?: any; started_at?: Date; completed_at?: Date; created_at: Date; updated_at: Date; } export class BundleSessionRepository { async create(data: { bundleId: string; sessionId?: string; userId?: string; deviceId?: string; validatorIds?: string[]; ip?: string; userAgent?: string; referrer?: string; metadata?: any; }): Promise<KanadiBundleSession> { const result = await sql` INSERT INTO kanadi_bundle_sessions ( bundle_id, session_id, user_id, device_id, validator_ids, ip, user_agent, referrer, metadata, started_at, total_score ) VALUES ( ${data.bundleId}, ${data.sessionId || null}, ${data.userId || null}, ${data.deviceId || null}, ${data.validatorIds ? sql`ARRAY[${data.validatorIds.join(',')}]::TEXT[]` : null}, ${data.ip || null}, ${data.userAgent || null}, ${data.referrer || null}, ${data.metadata ? JSON.stringify(data.metadata) : null}, CURRENT_TIMESTAMP, 0 ) RETURNING * `; return result[0] as KanadiBundleSession; } async update(id: string, data: { result?: string; totalScore?: number; confidence?: number; completedAt?: Date; }): Promise<void> { await sql` UPDATE kanadi_bundle_sessions SET result = COALESCE(${data.result || null}, result), total_score = COALESCE(${data.totalScore || null}, total_score), confidence = COALESCE(${data.confidence || null}, confidence), completed_at = COALESCE(${data.completedAt || null}, completed_at), updated_at = CURRENT_TIMESTAMP WHERE id = ${id} `; } async findById(id: string): Promise<KanadiBundleSession | null> { const result = await sql` SELECT * FROM kanadi_bundle_sessions WHERE id = ${id} LIMIT 1 `; return result.length > 0 ? (result[0] as KanadiBundleSession) : null; } async findBySessionId(sessionId: string): Promise<KanadiBundleSession[]> { const result = await sql` SELECT * FROM kanadi_bundle_sessions WHERE session_id = ${sessionId} ORDER BY created_at DESC `; return result as KanadiBundleSession[]; } async findByTimeRange( startTime: Date, endTime: Date, ): Promise<KanadiBundleSession[]> { const result = await sql` SELECT * FROM kanadi_bundle_sessions WHERE created_at >= ${startTime.toISOString()} AND created_at <= ${endTime.toISOString()} ORDER BY created_at DESC `; return result as KanadiBundleSession[]; } async findByIP(ip: string, limit: number = 100): Promise<KanadiBundleSession[]> { const result = await sql` SELECT * FROM kanadi_bundle_sessions WHERE ip = ${ip} ORDER BY created_at DESC LIMIT ${limit} `; return result as KanadiBundleSession[]; } async detectVolumeAttack( timeWindowMinutes: number = 1, threshold: number = 10, ): Promise<Array<{ ip: string; count: number; first: Date; last: Date }>> { const result = await sql` SELECT ip, COUNT(*) as count, MIN(created_at) as first, MAX(created_at) as last FROM kanadi_bundle_sessions WHERE created_at >= CURRENT_TIMESTAMP - INTERVAL '${sql.raw(timeWindowMinutes.toString())} minutes' AND ip IS NOT NULL GROUP BY ip HAVING COUNT(*) >= ${threshold} ORDER BY count DESC `; return result as Array<{ ip: string; count: number; first: Date; last: Date }>; } async findSuspiciousPatterns( timeWindowMinutes: number = 5, ): Promise<Array<{ device_id: string; sessions: number; total_bundles: number; ips: string[]; }>> { const result = await sql` SELECT device_id, COUNT(DISTINCT session_id) as sessions, COUNT(*) as total_bundles, ARRAY_AGG(DISTINCT ip) as ips FROM kanadi_bundle_sessions WHERE created_at >= CURRENT_TIMESTAMP - INTERVAL '${sql.raw(timeWindowMinutes.toString())} minutes' AND device_id IS NOT NULL GROUP BY device_id HAVING COUNT(DISTINCT session_id) > 5 ORDER BY sessions DESC `; return result as Array<{ device_id: string; sessions: number; total_bundles: number; ips: string[]; }>; } async getStats(): Promise<{ total: number; byResult: { pass: number; fail: number; suspicious: number }; avgScore: number; avgConfidence: number; recentActivity: number; }> { const totalResult = await sql` SELECT COUNT(*) as count FROM kanadi_bundle_sessions `; const byResultResult = await sql` SELECT result, COUNT(*) as count FROM kanadi_bundle_sessions WHERE result IS NOT NULL GROUP BY result `; const avgResult = await sql` SELECT AVG(total_score) as avg_score, AVG(confidence) as avg_confidence FROM kanadi_bundle_sessions WHERE total_score IS NOT NULL `; const recentResult = await sql` SELECT COUNT(*) as count FROM kanadi_bundle_sessions WHERE created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour' `; const byResult = { pass: 0, fail: 0, suspicious: 0, }; for (const row of byResultResult) { const result = (row as any).result?.toLowerCase(); const count = Number((row as any).count || 0); if (result === "pass") byResult.pass = count; else if (result === "fail") byResult.fail = count; else if (result === "suspicious") byResult.suspicious = count; } return { total: Number(totalResult[0]?.count || 0), byResult, avgScore: Number(avgResult[0]?.avg_score || 0), avgConfidence: Number(avgResult[0]?.avg_confidence || 0), recentActivity: Number(recentResult[0]?.count || 0), }; } }