Files
stack/packages/mosaic/src/telemetry/consent-store.ts
jason.woltje a531029c5b
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
feat(mosaic): mosaic telemetry command (M6 CU-06-01..05) (#417)
2026-04-05 07:06:42 +00:00

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);
}