import * as FS from "node:fs"; import * as Path from "node:path"; import { ClientSettingsSchema, type ClientSettings, type PersistedSavedEnvironmentRecord, } from "@t3tools/contracts"; import { Predicate } from "effect"; import * as Schema from "effect/Schema"; interface ClientSettingsDocument { readonly settings: ClientSettings; } interface PersistedSavedEnvironmentStorageRecord extends PersistedSavedEnvironmentRecord { readonly encryptedBearerToken?: string; } interface SavedEnvironmentRegistryDocument { readonly records: readonly PersistedSavedEnvironmentStorageRecord[]; } export interface DesktopSecretStorage { readonly isEncryptionAvailable: () => boolean; readonly encryptString: (value: string) => Buffer; readonly decryptString: (value: Buffer) => string; } function readJsonFile(filePath: string): T | null { try { if (!FS.existsSync(filePath)) { return null; } return JSON.parse(FS.readFileSync(filePath, "utf8")) as T; } catch { return null; } } function writeJsonFile(filePath: string, value: unknown): void { const directory = Path.dirname(filePath); const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`; FS.mkdirSync(directory, { recursive: true }); FS.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); FS.renameSync(tempPath, filePath); } function isPersistedSavedEnvironmentStorageRecord( value: unknown, ): value is PersistedSavedEnvironmentStorageRecord { return ( Predicate.isObject(value) && typeof value.environmentId === "string" && typeof value.label === "string" && typeof value.httpBaseUrl === "string" && typeof value.wsBaseUrl === "string" && typeof value.createdAt === "string" && (value.lastConnectedAt === null || typeof value.lastConnectedAt === "string") && (value.encryptedBearerToken === undefined || typeof value.encryptedBearerToken === "string") ); } function readSavedEnvironmentRegistryDocument(filePath: string): SavedEnvironmentRegistryDocument { const parsed = readJsonFile(filePath); if (!Predicate.isObject(parsed)) { return { records: [] }; } return { records: Array.isArray(parsed.records) ? parsed.records.filter(isPersistedSavedEnvironmentStorageRecord) : [], }; } function toPersistedSavedEnvironmentRecord( record: PersistedSavedEnvironmentStorageRecord, ): PersistedSavedEnvironmentRecord { return { environmentId: record.environmentId, label: record.label, httpBaseUrl: record.httpBaseUrl, wsBaseUrl: record.wsBaseUrl, createdAt: record.createdAt, lastConnectedAt: record.lastConnectedAt, }; } export function readClientSettings(settingsPath: string): ClientSettings | null { const raw = readJsonFile(settingsPath)?.settings; if (!raw) { return null; } try { return Schema.decodeUnknownSync(ClientSettingsSchema)(raw); } catch { return null; } } export function writeClientSettings(settingsPath: string, settings: ClientSettings): void { writeJsonFile(settingsPath, { settings } satisfies ClientSettingsDocument); } export function readSavedEnvironmentRegistry( registryPath: string, ): readonly PersistedSavedEnvironmentRecord[] { return readSavedEnvironmentRegistryDocument(registryPath).records.map((record) => toPersistedSavedEnvironmentRecord(record), ); } export function writeSavedEnvironmentRegistry( registryPath: string, records: readonly PersistedSavedEnvironmentRecord[], ): void { const currentDocument = readSavedEnvironmentRegistryDocument(registryPath); const encryptedBearerTokenById = new Map( currentDocument.records.flatMap((record) => record.encryptedBearerToken ? [[record.environmentId, record.encryptedBearerToken] as const] : [], ), ); writeJsonFile(registryPath, { records: records.map((record) => { const encryptedBearerToken = encryptedBearerTokenById.get(record.environmentId); return encryptedBearerToken ? { environmentId: record.environmentId, label: record.label, httpBaseUrl: record.httpBaseUrl, wsBaseUrl: record.wsBaseUrl, createdAt: record.createdAt, lastConnectedAt: record.lastConnectedAt, encryptedBearerToken, } : record; }), } satisfies SavedEnvironmentRegistryDocument); } export function readSavedEnvironmentSecret(input: { readonly registryPath: string; readonly environmentId: string; readonly secretStorage: DesktopSecretStorage; }): string | null { const document = readSavedEnvironmentRegistryDocument(input.registryPath); const encoded = document.records.find( (record) => record.environmentId === input.environmentId, )?.encryptedBearerToken; if (!encoded) { return null; } if (!input.secretStorage.isEncryptionAvailable()) { return null; } try { return input.secretStorage.decryptString(Buffer.from(encoded, "base64")); } catch { return null; } } export function writeSavedEnvironmentSecret(input: { readonly registryPath: string; readonly environmentId: string; readonly secret: string; readonly secretStorage: DesktopSecretStorage; }): boolean { const document = readSavedEnvironmentRegistryDocument(input.registryPath); if (!input.secretStorage.isEncryptionAvailable()) { return false; } let found = false; writeJsonFile(input.registryPath, { records: document.records.map((record) => { if (record.environmentId !== input.environmentId) { return record; } found = true; const encryptedBearerToken = input.secretStorage .encryptString(input.secret) .toString("base64"); return { environmentId: record.environmentId, label: record.label, httpBaseUrl: record.httpBaseUrl, wsBaseUrl: record.wsBaseUrl, createdAt: record.createdAt, lastConnectedAt: record.lastConnectedAt, encryptedBearerToken, } satisfies PersistedSavedEnvironmentStorageRecord; }), } satisfies SavedEnvironmentRegistryDocument); return found; } export function removeSavedEnvironmentSecret(input: { readonly registryPath: string; readonly environmentId: string; }): void { const document = readSavedEnvironmentRegistryDocument(input.registryPath); if ( !document.records.some( (record) => record.environmentId === input.environmentId && record.encryptedBearerToken !== undefined, ) ) { return; } writeJsonFile(input.registryPath, { records: document.records.map((record) => { if (record.environmentId !== input.environmentId) { return record; } return toPersistedSavedEnvironmentRecord(record); }), } satisfies SavedEnvironmentRegistryDocument); }