import type Database from 'better-sqlite3'; export interface BrowserSessionProfile { id: number; ownerId: string; label: string; startUrl: string; matchPatterns: string[]; storageOrigins: string[]; loggedInSelector: string | null; loginUrlPatterns: string[]; encryptedStateBlob: Buffer | null; stateVersion: number; playwrightVersion: string | null; status: 'pending' | 'active' | 'expired' | 'revoked' | 'error'; lastSavedAt: string | null; lastUsedAt: string | null; lastValidatedAt: string | null; lastError: string | null; createdAt: string; updatedAt: string; } export interface CreateProfileInput { ownerId: string; label: string; startUrl: string; matchPatterns: string[]; storageOrigins: string[]; loggedInSelector?: string | null; loginUrlPatterns: string[]; } export interface AuditInput { actorUserId?: string | null; ownerId?: string | null; profileId?: number | null; action: | 'create' | 'save' | 'decrypt' | 'use' | 'delete' | 'expire' | 'revoke' | 'test' | 'login_start' | 'login_cancel'; taskId?: number | null; jobId?: string | null; result: 'success' | 'error'; reason?: string | null; } function rowToProfile(row: Record): BrowserSessionProfile { return { id: row['id'] as number, ownerId: row['owner_id'] as string, label: row['label'] as string, startUrl: row['start_url'] as string, matchPatterns: JSON.parse((row['match_patterns'] as string) || '[]') as string[], storageOrigins: JSON.parse((row['storage_origins'] as string) || '[]') as string[], loggedInSelector: (row['logged_in_selector'] as string | null) ?? null, loginUrlPatterns: JSON.parse((row['login_url_patterns'] as string) || '[]') as string[], encryptedStateBlob: (row['encrypted_state_blob'] as Buffer | null) ?? null, stateVersion: (row['state_version'] as number) ?? 0, playwrightVersion: (row['playwright_version'] as string | null) ?? null, status: row['status'] as BrowserSessionProfile['status'], lastSavedAt: (row['last_saved_at'] as string | null) ?? null, lastUsedAt: (row['last_used_at'] as string | null) ?? null, lastValidatedAt: (row['last_validated_at'] as string | null) ?? null, lastError: (row['last_error'] as string | null) ?? null, createdAt: row['created_at'] as string, updatedAt: row['updated_at'] as string, }; } export class BrowserSessionRepo { constructor(private readonly db: Database.Database) {} // ── DEK management ──────────────────────────────────────────────── setUserDek(userId: string, encryptedDek: Buffer): void { this.db.prepare(` INSERT INTO user_deks (user_id, encrypted_dek) VALUES (?, ?) ON CONFLICT(user_id) DO UPDATE SET encrypted_dek = excluded.encrypted_dek `).run(userId, encryptedDek); } getUserDek(userId: string): Buffer | null { const row = this.db.prepare('SELECT encrypted_dek FROM user_deks WHERE user_id = ?').get(userId) as { encrypted_dek: Buffer } | undefined; return row?.encrypted_dek ?? null; } // ── Profiles ────────────────────────────────────────────────────── createProfile(input: CreateProfileInput): number { const result = this.db.prepare(` INSERT INTO browser_session_profiles (owner_id, label, start_url, match_patterns, storage_origins, logged_in_selector, login_url_patterns, status) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending') `).run( input.ownerId, input.label, input.startUrl, JSON.stringify(input.matchPatterns), JSON.stringify(input.storageOrigins), input.loggedInSelector ?? null, JSON.stringify(input.loginUrlPatterns), ); return Number(result.lastInsertRowid); } getProfileById(id: number, ownerId: string): BrowserSessionProfile | null { const row = this.db.prepare('SELECT * FROM browser_session_profiles WHERE id = ? AND owner_id = ?').get(id, ownerId) as Record | undefined; return row ? rowToProfile(row) : null; } /** Admin / worker path that does NOT enforce ownership. Caller must check elsewhere. */ getProfileByIdUnsafe(id: number): BrowserSessionProfile | null { const row = this.db.prepare('SELECT * FROM browser_session_profiles WHERE id = ?').get(id) as Record | undefined; return row ? rowToProfile(row) : null; } listProfilesByOwner(ownerId: string): BrowserSessionProfile[] { const rows = this.db.prepare('SELECT * FROM browser_session_profiles WHERE owner_id = ? ORDER BY label ASC').all(ownerId) as Array>; return rows.map(rowToProfile); } saveProfileBlob(id: number, encrypted: Buffer, playwrightVersion: string): void { this.db.prepare(` UPDATE browser_session_profiles SET encrypted_state_blob = ?, state_version = state_version + 1, playwright_version = ?, status = 'active', last_saved_at = datetime('now'), last_validated_at = datetime('now'), last_error = NULL, updated_at = datetime('now') WHERE id = ? `).run(encrypted, playwrightVersion, id); } markProfileStatus(id: number, status: BrowserSessionProfile['status'], reason: string | null = null): void { this.db.prepare(` UPDATE browser_session_profiles SET status = ?, last_error = ?, updated_at = datetime('now') WHERE id = ? `).run(status, reason, id); } touchUsed(id: number): void { this.db.prepare(`UPDATE browser_session_profiles SET last_used_at = datetime('now') WHERE id = ?`).run(id); } deleteProfile(id: number, ownerId: string): boolean { const result = this.db.prepare('DELETE FROM browser_session_profiles WHERE id = ? AND owner_id = ?').run(id, ownerId); return result.changes > 0; } // ── Audit ───────────────────────────────────────────────────────── audit(input: AuditInput): void { this.db.prepare(` INSERT INTO browser_session_audit (actor_user_id, profile_id, owner_id, action, task_id, job_id, result, reason) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `).run( input.actorUserId ?? null, input.profileId ?? null, input.ownerId ?? null, input.action, input.taskId ?? null, input.jobId ?? null, input.result, input.reason ?? null, ); } }