174 lines
6.5 KiB
TypeScript
174 lines
6.5 KiB
TypeScript
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<string, unknown>): 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<string, unknown> | 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<string, unknown> | 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<Record<string, unknown>>;
|
|
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,
|
|
);
|
|
}
|
|
}
|