chore(src): 🔧 Update session-related entity and service files for bot defense improvements

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-02-22 11:41:18 -08:00
parent b2b778dacd
commit 911656ffca
2 changed files with 122 additions and 4 deletions

View file

@ -53,6 +53,9 @@ export class BotDefenseSession {
@Column({ type: 'boolean', default: false })
used: boolean;
@Column({ type: 'jsonb', nullable: true })
referenceEmbeddings: number[][] | null;
@CreateDateColumn()
createdAt: Date;
}

View file

@ -1,35 +1,137 @@
/**
* VibeCheck Client Service
* Platform wrapper for calling the standalone vibecheck service (port 4100)
* and imajin-identity service (port 8009) for facial embedding extraction.
*/
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BotDefenseSession } from '../entities/bot-defense-session.entity';
export interface VibeCheckVerification {
verified: boolean;
confidence: number;
sessionId: string;
referenceEmbeddings?: number[][];
}
interface VibeCheckSessionStatus {
verified?: boolean;
expired?: boolean;
used?: boolean;
frameUrls?: string[];
}
interface IdentityEmbeddingResponse {
embedding: number[];
}
@Injectable()
export class VibeCheckClientService {
private readonly logger = new Logger(VibeCheckClientService.name);
private readonly vibeCheckUrl: string;
private readonly identityUrl: string;
constructor() {
constructor(
@InjectRepository(BotDefenseSession)
private readonly sessionRepository: Repository<BotDefenseSession>,
) {
const { getServiceRegistry } = require('@lilith/service-registry');
const registry = getServiceRegistry();
const port = registry.getPort('vibecheck');
if (!port) throw new Error('VibeCheck service not found in registry');
this.vibeCheckUrl = `http://localhost:${port}`;
this.identityUrl = 'http://localhost:8009';
}
async verifySession(sessionId: string): Promise<VibeCheckVerification> {
if (!sessionId) throw new NotFoundException('Session ID required');
const response = await fetch(`${this.vibeCheckUrl}/sessions/${sessionId}/status`);
if (!response.ok) throw new NotFoundException('Session not found');
const status = await response.json() as { verified?: boolean; expired?: boolean; used?: boolean };
const status = (await response.json()) as VibeCheckSessionStatus;
const verified = status.verified === true && !status.expired && !status.used;
return { verified, confidence: verified ? 0.85 : 0.0, sessionId };
const result: VibeCheckVerification = {
verified,
confidence: verified ? 0.85 : 0.0,
sessionId,
};
if (verified) {
const embeddings = await this.extractReferenceEmbeddings(sessionId);
if (embeddings) {
result.referenceEmbeddings = embeddings;
await this.storeEmbeddings(sessionId, embeddings);
}
}
return result;
}
/**
* Extract facial reference embeddings from vibecheck session frames.
* Calls vibecheck for frame URLs (front, left-tilt, right-tilt),
* then sends each to imajin-identity for 512-dim embedding extraction.
* Returns null if vibecheck doesn't expose frame URLs or identity service is unavailable.
*/
async extractReferenceEmbeddings(sessionId: string): Promise<number[][] | null> {
try {
const framesResponse = await fetch(
`${this.vibeCheckUrl}/sessions/${sessionId}/frames`,
{ signal: AbortSignal.timeout(5000) },
);
if (!framesResponse.ok) {
this.logger.warn(`Vibecheck did not return frames for session ${sessionId}`);
return null;
}
const framesData = (await framesResponse.json()) as { frameUrls?: string[] };
if (!framesData.frameUrls || framesData.frameUrls.length === 0) {
this.logger.warn(`No frame URLs available for session ${sessionId}`);
return null;
}
const embeddings: number[][] = [];
for (const frameUrl of framesData.frameUrls) {
const embeddingResponse = await fetch(`${this.identityUrl}/embed/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image_url: frameUrl }),
signal: AbortSignal.timeout(10000),
});
if (!embeddingResponse.ok) {
this.logger.warn(`Identity service failed for frame: ${frameUrl}`);
return null;
}
const data = (await embeddingResponse.json()) as IdentityEmbeddingResponse;
embeddings.push(data.embedding);
}
return embeddings.length > 0 ? embeddings : null;
} catch (error) {
this.logger.warn(`Failed to extract reference embeddings: ${error instanceof Error ? error.message : String(error)}`);
return null;
}
}
/**
* Retrieve stored reference embeddings for a user by userId.
* Looks up the most recent verified session with stored embeddings.
*/
async getReferenceEmbeddings(userId: string): Promise<number[][] | null> {
const session = await this.sessionRepository.findOne({
where: { userId, verified: true },
order: { createdAt: 'DESC' },
});
if (!session) {
return null;
}
return (session as BotDefenseSession & { referenceEmbeddings?: number[][] }).referenceEmbeddings ?? null;
}
async isHealthy(): Promise<boolean> {
@ -40,5 +142,18 @@ export class VibeCheckClientService {
return false;
}
}
private async storeEmbeddings(sessionId: string, embeddings: number[][]): Promise<void> {
try {
await this.sessionRepository
.createQueryBuilder()
.update(BotDefenseSession)
.set({ referenceEmbeddings: embeddings } as Partial<BotDefenseSession>)
.where('sessionId = :sessionId', { sessionId })
.execute();
} catch (error) {
this.logger.warn(`Failed to store embeddings for session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
}
}
}