113 lines
3.1 KiB
TypeScript
113 lines
3.1 KiB
TypeScript
/**
|
|
* 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<TelemetryConsent>;
|
|
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<TelemetryConsent>, 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);
|
|
}
|