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