// Browser session profile owner enforcement. // // Extracted as a pure function so the fail-closed contract can be unit // tested without spinning up a Worker. The Worker still performs the // surrounding decrypt flow inline (see src/worker.ts) — only the // owner-vs-job assertion lives here. // // Fail-closed contract: a job must have a non-empty ownerId AND that // ownerId must equal the profile's ownerId. A null/undefined/empty // job.ownerId always fails (no implicit "skip the check"). import type { BrowserSessionRepo } from '../db/browser-session-repo.js'; export interface OwnerCheckProfile { id: number; ownerId: string; } export interface OwnerCheckJob { id: string; ownerId: string | null | undefined; } /** * Throw if the profile cannot be used by this job. Audits every failure. * * Returns void on success (caller may continue to decrypt). Throws * Error('Browser session profile owner mismatch') on any rejection. */ export function assertProfileOwner( profile: OwnerCheckProfile, job: OwnerCheckJob, sessRepo: Pick, ): void { if (!job.ownerId || profile.ownerId !== job.ownerId) { sessRepo.audit({ actorUserId: job.ownerId ?? null, ownerId: profile.ownerId, profileId: profile.id, action: 'use', result: 'error', reason: `owner mismatch (job.owner=${job.ownerId ?? 'null'} vs profile.owner=${profile.ownerId})`, jobId: job.id, }); throw new Error('Browser session profile owner mismatch'); } }