const crypto = require('crypto'); const SECRET = process.env.FILE_SIGN_SECRET || process.env.SESSION_SECRET || 'file-sign-secret'; const EXPIRY_SECONDS = 15 * 60; // 15 minutes /** * Generate a signed URL for a protected file * @param {string} baseUrl - e.g. "https://host" * @param {string} filename - basename of the file * @returns {string} full signed URL */ function generateSignedUrl(baseUrl, filename) { const expires = Math.floor(Date.now() / 1000) + EXPIRY_SECONDS; const payload = `${filename}:${expires}`; const token = crypto .createHmac('sha256', SECRET) .update(payload) .digest('base64url'); return `${baseUrl}/secure-files/${encodeURIComponent(filename)}?token=${token}&expires=${expires}`; } /** * Verify a signed URL token * @param {string} filename * @param {string} token * @param {number} expires - unix timestamp * @returns {{ valid: boolean, reason?: string }} */ function verifySignedUrl(filename, token, expires) { const now = Math.floor(Date.now() / 1000); if (!token || !expires) { return { valid: false, reason: 'Missing token or expires' }; } if (now > Number(expires)) { return { valid: false, reason: 'URL has expired' }; } const payload = `${filename}:${expires}`; const expected = crypto .createHmac('sha256', SECRET) .update(payload) .digest('base64url'); const valid = crypto.timingSafeEqual( Buffer.from(token), Buffer.from(expected) ); return valid ? { valid: true } : { valid: false, reason: 'Invalid token' }; } module.exports = { generateSignedUrl, verifySignedUrl };