import { randomUUID } from "node:crypto"; import { decryptSecret, encryptSecret } from "../security/crypto"; export type F1tvAccountHealth = "ok" | "dead" | "unknown"; export type F1tvAccountStored = { id: string; label: string; email: string; /** Optioneel — voor latere browser-assisted login. */ passwordEnc?: string; ascendontokenEnc?: string; entitlementtokenEnc?: string; sessionId?: string; enabled: boolean; healthStatus?: F1tvAccountHealth; healthCheckedAt?: string; healthError?: string; }; export type F1tvAccountPublic = { id: string; label: string; email: string; hasPassword: boolean; hasAscendonToken: boolean; hasEntitlementToken: boolean; hasSessionId: boolean; enabled: boolean; sessionCount: number; healthStatus: F1tvAccountHealth; healthCheckedAt: string | null; healthError: string | null; }; export type F1tvAccountCreds = { id: string; label: string; email: string; password: string | null; ascendontoken: string | null; entitlementtoken: string | null; sessionId: string | null; enabled: boolean; }; /** contentId → accountIds die dit item mogen spelen. */ export type F1tvContentOwners = Record; const rrCursor = new Map(); export function accountToPublic( account: F1tvAccountStored, contentOwners: F1tvContentOwners ): F1tvAccountPublic { let sessionCount = 0; for (const owners of Object.values(contentOwners)) { if (owners.includes(account.id)) sessionCount += 1; } return { id: account.id, label: account.label || account.email, email: account.email, hasPassword: !!account.passwordEnc, hasAscendonToken: !!account.ascendontokenEnc, hasEntitlementToken: !!account.entitlementtokenEnc, hasSessionId: !!account.sessionId?.trim(), enabled: account.enabled !== false, sessionCount, healthStatus: account.healthStatus ?? "unknown", healthCheckedAt: account.healthCheckedAt ?? null, healthError: account.healthError ?? null, }; } export function decryptAccount( account: F1tvAccountStored, sessionSecret: string ): F1tvAccountCreds { return { id: account.id, label: account.label || account.email, email: account.email, password: account.passwordEnc ? decryptSecret(account.passwordEnc, sessionSecret) : null, ascendontoken: account.ascendontokenEnc ? decryptSecret(account.ascendontokenEnc, sessionSecret) : null, entitlementtoken: account.entitlementtokenEnc ? decryptSecret(account.entitlementtokenEnc, sessionSecret) : null, sessionId: account.sessionId?.trim() || null, enabled: account.enabled !== false, }; } export function createAccountStored( sessionSecret: string, input: { email: string; password?: string; label?: string; enabled?: boolean; id?: string; ascendontoken?: string; entitlementtoken?: string; sessionId?: string; } ): F1tvAccountStored { const email = input.email.trim().toLowerCase(); if (!email) throw new Error("E-mail verplicht"); const password = input.password?.trim(); const ascendontoken = input.ascendontoken?.trim(); const entitlementtoken = input.entitlementtoken?.trim(); if (!password && !ascendontoken && !entitlementtoken) { throw new Error("Wachtwoord of tokens verplicht"); } return { id: input.id?.trim() || randomUUID(), label: (input.label ?? "").trim() || email, email, passwordEnc: password ? encryptSecret(password, sessionSecret) : undefined, ascendontokenEnc: ascendontoken ? encryptSecret(ascendontoken, sessionSecret) : undefined, entitlementtokenEnc: entitlementtoken ? encryptSecret(entitlementtoken, sessionSecret) : undefined, sessionId: input.sessionId?.trim() || undefined, enabled: input.enabled !== false, }; } export function parseAccountsJson(raw: unknown): F1tvAccountStored[] { if (!Array.isArray(raw)) return []; const out: F1tvAccountStored[] = []; for (const item of raw) { if (!item || typeof item !== "object") continue; const row = item as Record; const id = String(row.id ?? "").trim(); const email = String(row.email ?? "").trim().toLowerCase(); if (!id || !email) continue; out.push({ id, label: String(row.label ?? "").trim() || email, email, passwordEnc: String(row.passwordEnc ?? "").trim() || undefined, ascendontokenEnc: String(row.ascendontokenEnc ?? "").trim() || undefined, entitlementtokenEnc: String(row.entitlementtokenEnc ?? "").trim() || undefined, sessionId: String(row.sessionId ?? "").trim() || undefined, enabled: row.enabled !== false, healthStatus: row.healthStatus === "ok" || row.healthStatus === "dead" || row.healthStatus === "unknown" ? row.healthStatus : undefined, healthCheckedAt: typeof row.healthCheckedAt === "string" ? row.healthCheckedAt : undefined, healthError: typeof row.healthError === "string" ? row.healthError : undefined, }); } return out; } export function parseContentOwners(raw: unknown): F1tvContentOwners { if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}; const out: F1tvContentOwners = {}; for (const [contentId, value] of Object.entries(raw as Record)) { if (!contentId.trim()) continue; const ids = Array.isArray(value) ? value.map((v) => String(v).trim()).filter(Boolean) : []; if (ids.length) out[contentId] = [...new Set(ids)]; } return out; } export function pickAccountIdForContent( contentId: string, ownerIds: string[], enabledAccountIds: string[] ): string | null { const pool = (ownerIds.length ? ownerIds : enabledAccountIds).filter((id) => enabledAccountIds.includes(id) ); if (!pool.length) return null; const key = contentId || "__global__"; const cursor = rrCursor.get(key) ?? 0; const chosen = pool[cursor % pool.length]; rrCursor.set(key, cursor + 1); return chosen; } export function accountHasPlayTokens(account: F1tvAccountStored): boolean { return !!(account.ascendontokenEnc && account.entitlementtokenEnc); }