import { BearerConnectionCredential, BearerConnectionProfile, BearerConnectionTarget, RelayConnectionTarget, SshConnectionProfile, SshConnectionTarget, } from "@t3tools/client-runtime/connection"; import { ConnectionCatalogDocument as RuntimeConnectionCatalogDocument, type ConnectionCatalogDocument as RuntimeConnectionCatalogDocumentType, } from "@t3tools/client-runtime/platform"; import type { PersistedSavedEnvironmentRecord } from "@t3tools/contracts"; import { fromLenientJson } from "@t3tools/shared/schemaJson"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; import * as ElectronSafeStorage from "../electron/ElectronSafeStorage.ts"; import * as DesktopSavedEnvironments from "../settings/DesktopSavedEnvironments.ts"; import * as DesktopEnvironment from "./DesktopEnvironment.ts"; const EncryptedConnectionCatalogDocument = Schema.Struct({ version: Schema.Literal(1), encryptedCatalog: Schema.String, }); type EncryptedConnectionCatalogDocument = typeof EncryptedConnectionCatalogDocument.Type; const EncryptedConnectionCatalogDocumentJson = fromLenientJson(EncryptedConnectionCatalogDocument); const decodeEncryptedConnectionCatalogDocumentJson = Schema.decodeEffect( EncryptedConnectionCatalogDocumentJson, ); const encodeEncryptedConnectionCatalogDocumentJson = Schema.encodeEffect( EncryptedConnectionCatalogDocumentJson, ); const RuntimeConnectionCatalogDocumentJson = Schema.fromJsonString( RuntimeConnectionCatalogDocument, ); const encodeRuntimeConnectionCatalogDocumentJson = Schema.encodeEffect( RuntimeConnectionCatalogDocumentJson, ); export class DesktopConnectionCatalogStoreWriteError extends Data.TaggedError( "DesktopConnectionCatalogStoreWriteError", )<{ readonly cause: PlatformError.PlatformError | Schema.SchemaError; }> { override get message() { return `Failed to write desktop connection catalog: ${this.cause.message}`; } } export class DesktopConnectionCatalogStoreDecodeError extends Data.TaggedError( "DesktopConnectionCatalogStoreDecodeError", )<{ readonly cause: Encoding.EncodingError; }> { override get message() { return "Failed to decode the desktop connection catalog."; } } export class DesktopConnectionCatalogStoreReadError extends Data.TaggedError( "DesktopConnectionCatalogStoreReadError", )<{ readonly cause: PlatformError.PlatformError | Schema.SchemaError; }> { override get message() { return `Failed to read desktop connection catalog: ${this.cause.message}`; } } export class DesktopConnectionCatalogStoreMigrationError extends Data.TaggedError( "DesktopConnectionCatalogStoreMigrationError", )<{ readonly cause: unknown; }> { override get message() { return "Failed to migrate legacy desktop saved environments."; } } export interface DesktopConnectionCatalogStoreShape { readonly get: Effect.Effect< Option.Option, | DesktopConnectionCatalogStoreReadError | DesktopConnectionCatalogStoreDecodeError | DesktopConnectionCatalogStoreMigrationError | ElectronSafeStorage.ElectronSafeStorageAvailabilityError | ElectronSafeStorage.ElectronSafeStorageDecryptError >; readonly set: ( catalog: string, ) => Effect.Effect< boolean, | DesktopConnectionCatalogStoreWriteError | ElectronSafeStorage.ElectronSafeStorageAvailabilityError | ElectronSafeStorage.ElectronSafeStorageEncryptError >; readonly clear: Effect.Effect; } export class DesktopConnectionCatalogStore extends Context.Service< DesktopConnectionCatalogStore, DesktopConnectionCatalogStoreShape >()("@t3tools/desktop/app/DesktopConnectionCatalogStore") {} function decodeSecretBytes( encoded: string, ): Effect.Effect { return Effect.fromResult(Encoding.decodeBase64(encoded)).pipe( Effect.mapError((cause) => new DesktopConnectionCatalogStoreDecodeError({ cause })), ); } const readDocument = ( fileSystem: FileSystem.FileSystem, catalogPath: string, ): Effect.Effect< Option.Option, PlatformError.PlatformError | Schema.SchemaError > => fileSystem.readFileString(catalogPath).pipe( Effect.catch((error) => error.reason._tag === "NotFound" ? Effect.succeed(null) : Effect.fail(error), ), Effect.flatMap((raw) => raw === null ? Effect.succeed(Option.none()) : decodeEncryptedConnectionCatalogDocumentJson(raw).pipe(Effect.map(Option.some)), ), ); const writeDocument = Effect.fn("desktop.connectionCatalogStore.writeDocument")(function* (input: { readonly fileSystem: FileSystem.FileSystem; readonly path: Path.Path; readonly catalogPath: string; readonly document: EncryptedConnectionCatalogDocument; readonly suffix: string; }): Effect.fn.Return { const directory = input.path.dirname(input.catalogPath); const tempPath = `${input.catalogPath}.${process.pid}.${input.suffix}.tmp`; const encoded = yield* encodeEncryptedConnectionCatalogDocumentJson(input.document); yield* input.fileSystem.makeDirectory(directory, { recursive: true }); yield* Effect.gen(function* () { yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`); yield* input.fileSystem.rename(tempPath, input.catalogPath); }).pipe( Effect.ensuring( input.fileSystem.remove(tempPath, { force: true }).pipe( Effect.catch((error) => Effect.logWarning("Could not remove a temporary connection catalog file.", { tempPath, error, }), ), ), ), ); }); function connectionId(prefix: "bearer" | "ssh", environmentId: string): string { return `${prefix}:${environmentId}`; } const migrateSavedEnvironmentRecords = Effect.fn( "desktop.connectionCatalogStore.migrateSavedEnvironmentRecords", )(function* ( records: readonly PersistedSavedEnvironmentRecord[], savedEnvironments: DesktopSavedEnvironments.DesktopSavedEnvironmentsShape, ): Effect.fn.Return< RuntimeConnectionCatalogDocumentType, DesktopSavedEnvironments.DesktopSavedEnvironmentsGetSecretError > { const targets: Array = []; const profiles: Array = []; const credentials: Array = []; for (const record of records) { if (record.relayManaged !== undefined) { targets.push( new RelayConnectionTarget({ environmentId: record.environmentId, label: record.label, }), ); continue; } if (record.desktopSsh !== undefined) { const id = connectionId("ssh", record.environmentId); targets.push( new SshConnectionTarget({ environmentId: record.environmentId, label: record.label, connectionId: id, }), ); profiles.push( new SshConnectionProfile({ connectionId: id, environmentId: record.environmentId, label: record.label, target: record.desktopSsh, }), ); continue; } const id = connectionId("bearer", record.environmentId); targets.push( new BearerConnectionTarget({ environmentId: record.environmentId, label: record.label, connectionId: id, }), ); profiles.push( new BearerConnectionProfile({ connectionId: id, environmentId: record.environmentId, label: record.label, httpBaseUrl: record.httpBaseUrl, wsBaseUrl: record.wsBaseUrl, }), ); const token = yield* savedEnvironments.getSecret(record.environmentId); if (Option.isSome(token)) { credentials.push({ connectionId: id, credential: new BearerConnectionCredential({ token: token.value }), }); } } return { schemaVersion: 1, targets, profiles, credentials, remoteDpopTokens: [], }; }); export const layer = Layer.effect( DesktopConnectionCatalogStore, Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const safeStorage = yield* ElectronSafeStorage.ElectronSafeStorage; const crypto = yield* Crypto.Crypto; const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments; const catalogPath = path.join(environment.stateDir, "connection-catalog.json"); const writeCatalog = Effect.fn("desktop.connectionCatalogStore.writeCatalog")(function* ( catalog: string, ) { const encryptedCatalog = Encoding.encodeBase64(yield* safeStorage.encryptString(catalog)); const suffix = (yield* crypto.randomUUIDv4.pipe( Effect.mapError((cause) => new DesktopConnectionCatalogStoreWriteError({ cause })), )).replace(/-/g, ""); yield* writeDocument({ fileSystem, path, catalogPath, document: { version: 1, encryptedCatalog }, suffix, }).pipe(Effect.mapError((cause) => new DesktopConnectionCatalogStoreWriteError({ cause }))); }); const migrateLegacyCatalog = Effect.gen(function* () { if (!(yield* safeStorage.isEncryptionAvailable)) { return Option.none(); } const records = yield* savedEnvironments.getRegistry; if (records.length === 0) { return Option.none(); } const catalog = yield* migrateSavedEnvironmentRecords(records, savedEnvironments); const encoded = yield* encodeRuntimeConnectionCatalogDocumentJson(catalog); yield* writeCatalog(encoded); return Option.some(encoded); }).pipe(Effect.mapError((cause) => new DesktopConnectionCatalogStoreMigrationError({ cause }))); return DesktopConnectionCatalogStore.of({ get: Effect.gen(function* () { const document = yield* readDocument(fileSystem, catalogPath).pipe( Effect.mapError((cause) => new DesktopConnectionCatalogStoreReadError({ cause })), ); if (Option.isNone(document)) { return yield* migrateLegacyCatalog; } if (!(yield* safeStorage.isEncryptionAvailable)) { return Option.none(); } const decrypted = yield* decodeSecretBytes(document.value.encryptedCatalog).pipe( Effect.flatMap(safeStorage.decryptString), ); return Option.some(decrypted); }).pipe(Effect.withSpan("desktop.connectionCatalogStore.get")), set: Effect.fn("desktop.connectionCatalogStore.set")(function* (catalog) { if (!(yield* safeStorage.isEncryptionAvailable)) { return false; } yield* writeCatalog(catalog); return true; }), clear: fileSystem.remove(catalogPath, { force: true }).pipe( Effect.catch((error) => Effect.logWarning("Could not clear the desktop connection catalog.", { catalogPath, error, }), ), Effect.withSpan("desktop.connectionCatalogStore.clear"), ), }); }), );