// The rules for a read-only link: it can expire, and it can need a passcode. Kept separate from the routes // so they can be tested on their own. import bcrypt from 'bcryptjs'; export const MAX_PASSCODE = 100; // "never" (null/0) or a number of days, up to a year -> an expiry time in ms (or null) export function expiryFromDays(days, now = Date.now()) { const n = Number(days); if (!days || !Number.isFinite(n) || n <= 0) return null; return now + Math.min(365, Math.round(n)) * 86_400_000; } export async function hashPasscode(passcode) { const p = String(passcode ?? '').slice(0, MAX_PASSCODE); return p.trim() ? bcrypt.hash(p.trim(), 10) : null; } // -> { ok: true } | { ok: false, status: 410, error } (expired) | { ok: false, status: 401, needsPasscode: true, error } export async function checkShareAccess({ expiresAt, passHash, supplied }, now = Date.now()) { if (expiresAt && expiresAt <= now) return { ok: false, status: 410, error: 'This link has expired. Ask the person who shared it for a new one.' }; if (!passHash) return { ok: true }; const given = String(supplied ?? '').slice(0, MAX_PASSCODE).trim(); if (!given) return { ok: false, status: 401, needsPasscode: true, error: 'This link needs a passcode.' }; if (!(await bcrypt.compare(given, passHash))) return { ok: false, status: 401, needsPasscode: true, error: 'That passcode is not right.' }; return { ok: true }; }