48 lines
1.5 KiB
TypeScript
48 lines
1.5 KiB
TypeScript
// 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<BrowserSessionRepo, 'audit'>,
|
|
): 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');
|
|
}
|
|
}
|