// Links for "Open in slicer". The slicer app downloads the file itself, outside // the browser and without the session cookie, so it gets a short-lived link that // grants just that one file. const crypto = require('crypto'); const path = require('path'); const jwt = require('jsonwebtoken'); const TTL_SECONDS = 10 * 60; const AUDIENCE = 'printhive-handoff'; const KINDS = ['file', 'entry']; // What slicers open: meshes, projects and STEP (Bambu Studio / OrcaSlicer import STEP). const SLICEABLE = ['3mf', 'stl', 'obj', 'step', 'stp']; // Keyed off the session secret but never equal to it, so a hand-off link can't // pass as a login and a login can't pass as a hand-off link. const keyFor = (secret) => crypto.createHmac('sha256', secret).update('handoff').digest('hex'); function createToken(secret, { kind, id }, ttl = TTL_SECONDS) { if (!KINDS.includes(kind)) throw new Error(`Unknown hand-off kind: ${kind}`); return jwt.sign({ k: kind, id: Number(id) }, keyFor(secret), { audience: AUDIENCE, expiresIn: ttl, algorithm: 'HS256' }); } // { kind, id }, or null for a link that's expired, altered or not a hand-off link. function readToken(secret, token) { try { const p = jwt.verify(String(token), keyFor(secret), { audience: AUDIENCE, algorithms: ['HS256'] }); return KINDS.includes(p.k) && Number.isInteger(p.id) ? { kind: p.k, id: p.id } : null; } catch { return null; } } // Slicers name the download after the link's last path segment and decode the // link in different ways, so that segment is kept to plain ASCII. function urlName(name) { const ext = path.extname(String(name)).toLowerCase().replace(/[^\w.]/g, ''); const base = path.basename(String(name), path.extname(String(name))) .normalize('NFKD') .replace(/[̀-ͯ]/g, '') // accents split off by NFKD: "é" -> "e" .replace(/[^\w.-]+/g, '_') .replace(/^[_.-]+|[_.-]+$/g, '') .slice(0, 80); return `${base || 'model'}${ext}`; } module.exports = { TTL_SECONDS, SLICEABLE, createToken, readToken, urlName };