/** * Persistent token store backed by a JSON file. * * Pure JavaScript — no native addons, no build tools required. * Survives process restarts so users are not logged out on redeploy. * * Tokens are stored in DATA_DIR/tokens.json (default: ./data locally, * /app/data in the container). Writes are atomic: data is written to a * temp file then renamed so a crash mid-write never corrupts the store. * * Format: { "": { accessToken: "...", createdAt: } } * * Expired entries (older than TOKEN_TTL_DAYS) are pruned on startup * and once per hour. */ const path = require('path'); const fs = require('fs'); const TOKEN_TTL_DAYS = 31; // slightly longer than max cookie lifetime (30d) const TOKEN_TTL_MS = TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000; const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '../../data'); if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } const STORE_PATH = path.join(DATA_DIR, 'tokens.json'); const STORE_TMP = STORE_PATH + '.tmp'; // Load store from disk, return empty object on any error function load() { try { return JSON.parse(fs.readFileSync(STORE_PATH, 'utf8')); } catch { return {}; } } // Atomic write: write to .tmp then rename to avoid partial-write corruption function save(data) { try { fs.writeFileSync(STORE_TMP, JSON.stringify(data), 'utf8'); fs.renameSync(STORE_TMP, STORE_PATH); } catch (err) { console.error('[TokenStore] Failed to persist token store:', err.message); } } function prune(data) { const cutoff = Date.now() - TOKEN_TTL_MS; let pruned = 0; for (const userId of Object.keys(data)) { if (data[userId].createdAt < cutoff) { delete data[userId]; pruned++; } } if (pruned > 0) { console.log(`[TokenStore] Pruned ${pruned} expired token(s)`); } return data; } // Prune on startup let store = prune(load()); save(store); // Prune once per hour (unref so it doesn't keep the process alive) setInterval(() => { store = prune(load()); save(store); }, 60 * 60 * 1000).unref(); module.exports = { storeToken(userId, accessToken) { store[userId] = { accessToken, createdAt: Date.now() }; save(store); }, getToken(userId) { const entry = store[userId]; if (!entry) return null; // Also honour TTL on read in case pruning hasn't run yet if (Date.now() - entry.createdAt > TOKEN_TTL_MS) { delete store[userId]; save(store); return null; } return { accessToken: entry.accessToken }; }, clearToken(userId) { if (store[userId]) { delete store[userId]; save(store); } } };