/** * Persistent consent store for remote telemetry upload. * * State is stored in $MOSAIC_HOME/telemetry.json (not inside the markdown * config files — those are template-rendered and would lose structured data). * * Schema: * { * remoteEnabled: boolean, * optedInAt: string | null, // ISO timestamp * optedOutAt: string | null, // ISO timestamp * lastUploadAt: string | null // ISO timestamp * } */ import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { atomicWrite } from '../platform/file-ops.js'; import { DEFAULT_MOSAIC_HOME } from '../constants.js'; export interface TelemetryConsent { remoteEnabled: boolean; optedInAt: string | null; optedOutAt: string | null; lastUploadAt: string | null; } const TELEMETRY_FILE = 'telemetry.json'; const DEFAULT_CONSENT: TelemetryConsent = { remoteEnabled: false, optedInAt: null, optedOutAt: null, lastUploadAt: null, }; function consentFilePath(mosaicHome?: string): string { return join(mosaicHome ?? getMosaicHome(), TELEMETRY_FILE); } function getMosaicHome(): string { return process.env['MOSAIC_HOME'] ?? DEFAULT_MOSAIC_HOME; } /** * Read the current consent state. Returns defaults if file doesn't exist. */ export function readConsent(mosaicHome?: string): TelemetryConsent { const filePath = consentFilePath(mosaicHome); if (!existsSync(filePath)) { return { ...DEFAULT_CONSENT }; } try { const raw = readFileSync(filePath, 'utf-8'); const parsed = JSON.parse(raw) as Partial; return { remoteEnabled: parsed.remoteEnabled ?? false, optedInAt: parsed.optedInAt ?? null, optedOutAt: parsed.optedOutAt ?? null, lastUploadAt: parsed.lastUploadAt ?? null, }; } catch { return { ...DEFAULT_CONSENT }; } } /** * Persist a full or partial consent update. */ export function writeConsent(update: Partial, mosaicHome?: string): void { const current = readConsent(mosaicHome); const next: TelemetryConsent = { ...current, ...update }; atomicWrite(consentFilePath(mosaicHome), JSON.stringify(next, null, 2) + '\n'); } /** * Mark opt-in: enable remote upload and record timestamp. */ export function optIn(mosaicHome?: string): TelemetryConsent { const now = new Date().toISOString(); const next: TelemetryConsent = { remoteEnabled: true, optedInAt: now, optedOutAt: null, lastUploadAt: readConsent(mosaicHome).lastUploadAt, }; writeConsent(next, mosaicHome); return next; } /** * Mark opt-out: disable remote upload and record timestamp. */ export function optOut(mosaicHome?: string): TelemetryConsent { const now = new Date().toISOString(); const current = readConsent(mosaicHome); const next: TelemetryConsent = { remoteEnabled: false, optedInAt: current.optedInAt, optedOutAt: now, lastUploadAt: current.lastUploadAt, }; writeConsent(next, mosaicHome); return next; } /** * Record a successful upload timestamp. */ export function recordUpload(mosaicHome?: string): void { writeConsent({ lastUploadAt: new Date().toISOString() }, mosaicHome); }