perf(bot-defense): Add optimized database indexes for bot defense queries

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-02-28 15:52:19 -08:00
parent 3c0f05660c
commit ce0183c92e
4 changed files with 106 additions and 84 deletions

View file

@ -1,5 +1,5 @@
import { DataSource } from 'typeorm';
import { AddBotDefenseIndexes1738827600000 } from './migrations/1738827600000-AddBotDefenseIndexes';
import { InitialSchema1700000000000 } from './migrations/1700000000000-InitialSchema';
/**
* TypeORM Data Source Configuration
@ -19,6 +19,6 @@ export const AppDataSource = new DataSource({
password: process.env.DATABASE_POSTGRES_PASSWORD || 'lilith',
database: process.env.DATABASE_POSTGRES_NAME || 'lilith_bot_defense',
entities: ['dist/**/*.entity.js'],
migrations: [AddBotDefenseIndexes1738827600000],
migrations: [InitialSchema1700000000000],
logging: true,
});

View file

@ -0,0 +1,103 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Initial schema for bot-defense feature.
*
* Creates:
* - bot_defense_sessions: Liveness verification sessions
* - bot_defense_attempts: Individual verification attempts per session
*
* Indexes:
* - userId (sessions) for user session lookups
* - (ipAddress, createdAt) for suspicious pattern detection
* - expiresAt for cleanup cron job queries
* - verifiedAt for verification history and analytics
* - (userId, createdAt) WHERE verified = true for isVerified() status checks (partial)
* - sessionId (attempts) for FK and lookup performance
*/
export class InitialSchema1700000000000 implements MigrationInterface {
name = 'InitialSchema1700000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
// Create bot_defense_sessions table
await queryRunner.query(`
CREATE TABLE "bot_defense_sessions" (
"sessionId" UUID NOT NULL DEFAULT gen_random_uuid(),
"userId" UUID NOT NULL,
"nonce" VARCHAR(64) NOT NULL,
"ipAddress" VARCHAR(45) NOT NULL,
"verified" BOOLEAN NOT NULL DEFAULT false,
"confidence" DECIMAL(4,3),
"verifiedAt" TIMESTAMP,
"expiresAt" TIMESTAMP NOT NULL,
"used" BOOLEAN NOT NULL DEFAULT false,
"referenceEmbeddings" JSONB,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "PK_bot_defense_sessions" PRIMARY KEY ("sessionId")
)
`);
// Create bot_defense_attempts table
await queryRunner.query(`
CREATE TABLE "bot_defense_attempts" (
"attemptId" UUID NOT NULL DEFAULT gen_random_uuid(),
"sessionId" UUID NOT NULL,
"success" BOOLEAN NOT NULL,
"confidence" DECIMAL(4,3) NOT NULL,
"errorDetails" JSONB,
"attemptedAt" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "PK_bot_defense_attempts" PRIMARY KEY ("attemptId"),
CONSTRAINT "FK_bot_defense_attempts_session" FOREIGN KEY ("sessionId")
REFERENCES "bot_defense_sessions" ("sessionId") ON DELETE CASCADE
)
`);
// Index: userId — for user session lookups
await queryRunner.query(`
CREATE INDEX "IDX_bot_defense_sessions_userId"
ON "bot_defense_sessions" ("userId")
`);
// Index: (ipAddress, createdAt) — for suspicious pattern detection (getRecentFailureCount)
await queryRunner.query(`
CREATE INDEX "IDX_bot_defense_sessions_ip_created"
ON "bot_defense_sessions" ("ipAddress", "createdAt")
`);
// Index: expiresAt — for cleanupExpiredSessions() cron job
await queryRunner.query(`
CREATE INDEX "IDX_bot_defense_sessions_expires_at"
ON "bot_defense_sessions" ("expiresAt")
`);
// Index: verifiedAt — for verification history and analytics queries
await queryRunner.query(`
CREATE INDEX "IDX_bot_defense_sessions_verified_at"
ON "bot_defense_sessions" ("verifiedAt")
`);
// Partial index: (userId, createdAt) WHERE verified = true — for isVerified() status checks
await queryRunner.query(`
CREATE INDEX "IDX_bot_defense_sessions_user_verified"
ON "bot_defense_sessions" ("userId", "createdAt")
WHERE "verified" = true
`);
// Index: sessionId on attempts — for FK lookups
await queryRunner.query(`
CREATE INDEX "IDX_bot_defense_attempts_sessionId"
ON "bot_defense_attempts" ("sessionId")
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_bot_defense_attempts_sessionId"`);
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_bot_defense_sessions_user_verified"`);
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_bot_defense_sessions_verified_at"`);
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_bot_defense_sessions_expires_at"`);
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_bot_defense_sessions_ip_created"`);
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_bot_defense_sessions_userId"`);
await queryRunner.query(`DROP TABLE IF EXISTS "bot_defense_attempts"`);
await queryRunner.query(`DROP TABLE IF EXISTS "bot_defense_sessions"`);
}
}

View file

@ -1,81 +0,0 @@
import { TableIndex } from 'typeorm';
import type { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Add Performance-Critical Indexes to bot_defense_sessions
*
* Indexes added based on query patterns in bot-defense.service.ts:
*
* 1. Composite (ipAddress, createdAt) - For getRecentFailureCount() suspicious pattern detection
* - Query: WHERE ipAddress = ? AND createdAt >= ? AND verified = false
* - Used after MAX_ATTEMPTS reached to detect bot farms
*
* 2. expiresAt - For cleanupExpiredSessions() cron job
* - Query: WHERE expiresAt < NOW()
* - Runs regularly to remove stale sessions
*
* 3. verifiedAt - For verification history and analytics queries
* - Query: WHERE verifiedAt IS NOT NULL ORDER BY verifiedAt DESC
* - Used for compliance audits and success rate monitoring
*
* 4. Partial index on (userId, verified) - For isVerified() status checks
* - Query: WHERE userId = ? AND verified = true ORDER BY createdAt DESC
* - Optimizes verification status lookups (only indexes verified=true rows)
*/
export class AddBotDefenseIndexes1738827600000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// 1. Composite index for suspicious pattern detection
await queryRunner.createIndex(
'bot_defense_sessions',
new TableIndex({
name: 'IDX_bot_defense_sessions_ip_created',
columnNames: ['ipAddress', 'createdAt'],
}),
);
// 2. Index for cleanup queries
await queryRunner.createIndex(
'bot_defense_sessions',
new TableIndex({
name: 'IDX_bot_defense_sessions_expires_at',
columnNames: ['expiresAt'],
}),
);
// 3. Index for verification history queries
await queryRunner.createIndex(
'bot_defense_sessions',
new TableIndex({
name: 'IDX_bot_defense_sessions_verified_at',
columnNames: ['verifiedAt'],
}),
);
// 4. Partial index for verified sessions (PostgreSQL-specific optimization)
// Only indexes rows where verified = true, reducing index size by ~95%
await queryRunner.query(`
CREATE INDEX "IDX_bot_defense_sessions_user_verified"
ON "bot_defense_sessions" ("userId", "createdAt")
WHERE "verified" = true
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Drop indexes in reverse order
await queryRunner.query(
'DROP INDEX IF EXISTS "IDX_bot_defense_sessions_user_verified"',
);
await queryRunner.dropIndex(
'bot_defense_sessions',
'IDX_bot_defense_sessions_verified_at',
);
await queryRunner.dropIndex(
'bot_defense_sessions',
'IDX_bot_defense_sessions_expires_at',
);
await queryRunner.dropIndex(
'bot_defense_sessions',
'IDX_bot_defense_sessions_ip_created',
);
}
}

View file

@ -4,4 +4,4 @@
* Export all migrations for TypeORM discovery
*/
export { AddBotDefenseIndexes1738827600000 } from './1738827600000-AddBotDefenseIndexes';
export { InitialSchema1700000000000 } from './1700000000000-InitialSchema';