Files
deepseek-harness/apps/desktop/src/update-journal.ts
T

75 lines
3.5 KiB
TypeScript

/** Opt-in qualification evidence outside the installation directory; no raw diagnostics or request data. */
import { randomUUID } from 'node:crypto'
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
import { isAbsolute, join } from 'node:path'
import type { DesktopUpdateState } from './ipc.ts'
/** User and process milestones that connect update states across application restarts. */
export type DesktopUpdateJournalAction = 'started' | 'workspace-ready' | 'workspace-failed'
| 'check-requested' | 'download-requested' | 'install-confirmed' | 'quit-requested'
| 'policy-login-opened' | 'policy-login-returned' | 'policy-login-cancelled' | 'policy-login-failed'
const ERROR_CODES = ['ETIMEDOUT', 'ENOSPC', 'ERR_INTERNET_DISCONNECTED', 'ERR_CONNECTION_RESET',
'ERR_CONNECTION_CLOSED', 'ERR_NAME_NOT_RESOLVED', 'ERR_UPDATER_INVALID_SIGNATURE', 'ERR_UPDATER_CHECKSUM_MISMATCH'] as const
/**
* Whitelist one update state for disk; neither error text nor unexpected object fields survive.
* @param state Main-process-owned update state.
* @returns Only phase, target version, integer progress, operation, and a fixed error classification.
*/
export function desktopUpdateJournalState(state: DesktopUpdateState): object {
return {
phase: state.phase,
...(state.version !== undefined ? { targetVersion: state.version } : {}),
...(state.phase === 'downloading' && state.percent !== undefined ? { percent: Math.floor(state.percent) } : {}),
...(state.phase === 'error' ? {
failedOperation: state.failedOperation,
errorCode: ERROR_CODES.find(code => state.message?.includes(code)) ?? 'UNCLASSIFIED',
} : {}),
}
}
/** Process-owned JSONL evidence; each append is flushed before the caller continues. */
export class DesktopUpdateJournal {
readonly path: string
private sequence = 0
private previousState: string | undefined
/**
* @param directory Absolute evidence directory, retained across installs; creation errors stop qualification.
* @param version Installed application version, not a version supplied by the feed.
*/
constructor(directory: string, private readonly version: string) {
if (!isAbsolute(directory)) throw new Error('desktop update journal: directory must be absolute')
mkdirSync(directory, { recursive: true })
this.path = join(directory, `${Date.now()}-${randomUUID()}.jsonl`)
writeFileSync(this.path, '', { flag: 'wx', mode: 0o600, flush: true })
this.action('started')
}
/**
* Append a fixed action; failures propagate so incomplete qualification is never reported as traced.
* @param action Update operation or process milestone; no free-text fields are accepted.
* @returns Nothing after the record has been flushed.
*/
action(action: DesktopUpdateJournalAction): void { this.append({ event: action }) }
/**
* Retain state changes and integer progress increments without raw errors, URLs, or request data.
* @param state Main-process-owned update state.
* @returns Nothing after the changed state has been flushed; duplicate states add no record.
*/
state(state: DesktopUpdateState): void {
const fields = desktopUpdateJournalState(state)
const encoded = JSON.stringify(fields)
if (encoded === this.previousState) return
this.append({ event: 'state', ...fields })
this.previousState = encoded
}
private append(fields: object): void {
appendFileSync(this.path, `${JSON.stringify({ schemaVersion: 1, sequence: this.sequence++,
time: new Date().toISOString(), pid: process.pid, version: this.version, ...fields })}\n`, { flush: true })
}
}