import { promises as fs } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createLogger } from './logger.js'; const log = createLogger('tesla-token-store'); export interface TeslaTokens { accessToken: string; refreshToken: string; expiresAt: number; // ms epoch scope: string; // Optional cached vehicle metadata so we don't have to refetch on every call. vehicleId?: string; // numeric id_s used in Fleet API URLs vin?: string; carType?: string; // e.g. modely trimBadging?: string; // e.g. lrawd connectedAt: number; } interface Store { get(userId: string): Promise; set(userId: string, tokens: TeslaTokens): Promise; remove(userId: string): Promise; } // File-backed store. Falls back to /app/data when running under Dokku (we // will mount a Dokku storage volume on /app/data), or to ./data in dev. const __dirname = path.dirname(fileURLToPath(import.meta.url)); const dataDir = process.env.TESLA_TOKEN_DIR || (process.env.NODE_ENV === 'production' ? '/app/data' : path.resolve(__dirname, '../../data')); const tokenFile = path.join(dataDir, 'tesla-tokens.json'); let cache: Record = {}; let loaded = false; let writeLock: Promise = Promise.resolve(); async function load(): Promise { if (loaded) return; try { await fs.mkdir(dataDir, { recursive: true }); const raw = await fs.readFile(tokenFile, 'utf8'); cache = JSON.parse(raw); log.info({ file: tokenFile, users: Object.keys(cache).length }, 'Loaded Tesla tokens'); } catch (err: any) { if (err.code !== 'ENOENT') log.warn({ err: String(err) }, 'Failed to load Tesla tokens — starting empty'); cache = {}; } loaded = true; } async function persist(): Promise { // Serialise writes so concurrent set/remove calls don't race. writeLock = writeLock .then(async () => { await fs.mkdir(dataDir, { recursive: true }); await fs.writeFile(tokenFile, JSON.stringify(cache, null, 2)); }) .catch(err => log.error({ err: String(err) }, 'Failed to persist Tesla tokens')); await writeLock; } export const teslaTokenStore: Store = { async get(userId) { await load(); return cache[userId] || null; }, async set(userId, tokens) { await load(); cache[userId] = tokens; await persist(); }, async remove(userId) { await load(); delete cache[userId]; await persist(); }, };