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 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 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,); const DesktopConnectionCatalogStoreWriteOperation = Schema.Literals([ "create-temporary-file-name", "encode-document", "create-directory", "write-temporary-file", "replace-catalog-file",]); const DesktopConnectionCatalogStoreMigrationOperation = Schema.Literals([ "read-legacy-registry", "read-legacy-secret", "encode-catalog", "persist-catalog",]); const DesktopConnectionCatalogStoreProtectionOperation = Schema.Literals([ "check-encryption-availability", "encrypt-catalog", "decrypt-catalog",]); export class DesktopConnectionCatalogStoreWriteError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreWriteError>()( "DesktopConnectionCatalogStoreWriteError", { operation: DesktopConnectionCatalogStoreWriteOperation, path: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return `Desktop connection catalog write failed during ${this.operation} at ${this.path}.`; }} export class DesktopConnectionCatalogStoreDecodeError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreDecodeError>()( "DesktopConnectionCatalogStoreDecodeError", { resource: Schema.Literal("encryptedCatalog"), catalogPath: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return `Failed to decode ${this.resource} for the desktop connection catalog at ${this.catalogPath}.`; }} export class DesktopConnectionCatalogStoreReadError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreReadError>()( "DesktopConnectionCatalogStoreReadError", { catalogPath: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return `Failed to read the desktop connection catalog at ${this.catalogPath}.`; }} export class DesktopConnectionCatalogStoreDocumentDecodeError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreDocumentDecodeError>()( "DesktopConnectionCatalogStoreDocumentDecodeError", { catalogPath: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return `Failed to decode the desktop connection catalog document at ${this.catalogPath}.`; }} export class DesktopConnectionCatalogStoreMigrationError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreMigrationError>()( "DesktopConnectionCatalogStoreMigrationError", { operation: DesktopConnectionCatalogStoreMigrationOperation, catalogPath: Schema.String, environmentId: Schema.optionalKey(Schema.String), cause: Schema.Defect(), },) { override get message(): string { const environment = this.environmentId === undefined ? "" : ` for environment ${this.environmentId}`; return `Legacy desktop saved-environment migration failed during ${this.operation}${environment} into ${this.catalogPath}.`; }} export class DesktopConnectionCatalogStoreProtectionError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreProtectionError>()( "DesktopConnectionCatalogStoreProtectionError", { operation: DesktopConnectionCatalogStoreProtectionOperation, catalogPath: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return `Desktop connection catalog protection failed during ${this.operation} at ${this.catalogPath}.`; }} export class DesktopConnectionCatalogStore extends Context.Service< DesktopConnectionCatalogStore, { readonly get: Effect.Effect< Option.Option<string>, | DesktopConnectionCatalogStoreReadError | DesktopConnectionCatalogStoreDocumentDecodeError | DesktopConnectionCatalogStoreDecodeError | DesktopConnectionCatalogStoreMigrationError | DesktopConnectionCatalogStoreProtectionError >; readonly set: ( catalog: string, ) => Effect.Effect< boolean, DesktopConnectionCatalogStoreWriteError | DesktopConnectionCatalogStoreProtectionError >; readonly clear: Effect.Effect<void>; }>()("@t3tools/desktop/app/DesktopConnectionCatalogStore") {} function decodeSecretBytes( catalogPath: string, encoded: string,): Effect.Effect<Uint8Array, DesktopConnectionCatalogStoreDecodeError> { return Effect.fromResult(Encoding.decodeBase64(encoded)).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreDecodeError({ resource: "encryptedCatalog", catalogPath, cause, }), ), );} const readDocument = ( fileSystem: FileSystem.FileSystem, catalogPath: string,): Effect.Effect< Option.Option<EncryptedConnectionCatalogDocument>, DesktopConnectionCatalogStoreReadError | DesktopConnectionCatalogStoreDocumentDecodeError> => fileSystem.readFileString(catalogPath).pipe( Effect.catch((error) => error.reason._tag === "NotFound" ? Effect.succeed<string | null>(null) : Effect.fail( new DesktopConnectionCatalogStoreReadError({ catalogPath, cause: error, }), ), ), Effect.flatMap((raw) => raw === null ? Effect.succeed(Option.none<EncryptedConnectionCatalogDocument>()) : decodeEncryptedConnectionCatalogDocumentJson(raw).pipe( Effect.map(Option.some), Effect.mapError( (cause) => new DesktopConnectionCatalogStoreDocumentDecodeError({ catalogPath, cause, }), ), ), ), ); 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<void, DesktopConnectionCatalogStoreWriteError> { const directory = input.path.dirname(input.catalogPath); const tempPath = `${input.catalogPath}.${process.pid}.${input.suffix}.tmp`; const encoded = yield* encodeEncryptedConnectionCatalogDocumentJson(input.document).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreWriteError({ operation: "encode-document", path: input.catalogPath, cause, }), ), ); yield* input.fileSystem.makeDirectory(directory, { recursive: true }).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreWriteError({ operation: "create-directory", path: directory, cause, }), ), ); yield* Effect.gen(function* () { yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreWriteError({ operation: "write-temporary-file", path: tempPath, cause, }), ), ); yield* input.fileSystem.rename(tempPath, input.catalogPath).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreWriteError({ operation: "replace-catalog-file", path: input.catalogPath, cause, }), ), ); }).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.DesktopSavedEnvironments["Service"], catalogPath: string,): Effect.fn.Return< RuntimeConnectionCatalogDocumentType, DesktopConnectionCatalogStoreMigrationError> { const targets: Array<RuntimeConnectionCatalogDocumentType["targets"][number]> = []; const profiles: Array<RuntimeConnectionCatalogDocumentType["profiles"][number]> = []; const credentials: Array<RuntimeConnectionCatalogDocumentType["credentials"][number]> = []; 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).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreMigrationError({ operation: "read-legacy-secret", catalogPath, environmentId: record.environmentId, cause, }), ), ); if (Option.isSome(token)) { credentials.push({ connectionId: id, credential: new BearerConnectionCredential({ token: token.value }), }); } } return { schemaVersion: 1, targets, profiles, credentials, remoteDpopTokens: [], };}); export const make = 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 encryptionAvailable = safeStorage.isEncryptionAvailable.pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreProtectionError({ operation: "check-encryption-availability", catalogPath, cause, }), ), ); const writeCatalog = Effect.fn("desktop.connectionCatalogStore.writeCatalog")(function* ( catalog: string, ) { const encryptedCatalog = Encoding.encodeBase64( yield* safeStorage.encryptString(catalog).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreProtectionError({ operation: "encrypt-catalog", catalogPath, cause, }), ), ), ); const suffix = (yield* crypto.randomUUIDv4.pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreWriteError({ operation: "create-temporary-file-name", path: catalogPath, cause, }), ), )).replace(/-/g, ""); yield* writeDocument({ fileSystem, path, catalogPath, document: { version: 1, encryptedCatalog }, suffix, }); }); const migrateLegacyCatalog = Effect.gen(function* () { if (!(yield* encryptionAvailable)) { return Option.none<string>(); } const records = yield* savedEnvironments.getRegistry.pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreMigrationError({ operation: "read-legacy-registry", catalogPath, cause, }), ), ); if (records.length === 0) { return Option.none<string>(); } const catalog = yield* migrateSavedEnvironmentRecords(records, savedEnvironments, catalogPath); const encoded = yield* encodeRuntimeConnectionCatalogDocumentJson(catalog).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreMigrationError({ operation: "encode-catalog", catalogPath, cause, }), ), ); yield* writeCatalog(encoded).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreMigrationError({ operation: "persist-catalog", catalogPath, cause, }), ), ); return Option.some(encoded); }); return DesktopConnectionCatalogStore.of({ get: Effect.gen(function* () { const document = yield* readDocument(fileSystem, catalogPath); if (Option.isNone(document)) { return yield* migrateLegacyCatalog; } if (!(yield* encryptionAvailable)) { return Option.none<string>(); } const decrypted = yield* decodeSecretBytes(catalogPath, document.value.encryptedCatalog).pipe( Effect.flatMap((encryptedCatalog) => safeStorage.decryptString(encryptedCatalog).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreProtectionError({ operation: "decrypt-catalog", catalogPath, cause, }), ), ), ), ); return Option.some(decrypted); }).pipe(Effect.withSpan("desktop.connectionCatalogStore.get")), set: Effect.fn("desktop.connectionCatalogStore.set")(function* (catalog) { if (!(yield* encryptionAvailable)) { return false; }Lines 1–500 of 516. Load more
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 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 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, ); const DesktopConnectionCatalogStoreWriteOperation = Schema.Literals([ "create-temporary-file-name", "encode-document", "create-directory", "write-temporary-file", "replace-catalog-file", ]); const DesktopConnectionCatalogStoreMigrationOperation = Schema.Literals([ "read-legacy-registry", "read-legacy-secret", "encode-catalog", "persist-catalog", ]); const DesktopConnectionCatalogStoreProtectionOperation = Schema.Literals([ "check-encryption-availability", "encrypt-catalog", "decrypt-catalog", ]); export class DesktopConnectionCatalogStoreWriteError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreWriteError>()( "DesktopConnectionCatalogStoreWriteError", { operation: DesktopConnectionCatalogStoreWriteOperation, path: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Desktop connection catalog write failed during ${this.operation} at ${this.path}.`; } } export class DesktopConnectionCatalogStoreDecodeError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreDecodeError>()( "DesktopConnectionCatalogStoreDecodeError", { resource: Schema.Literal("encryptedCatalog"), catalogPath: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to decode ${this.resource} for the desktop connection catalog at ${this.catalogPath}.`; } } export class DesktopConnectionCatalogStoreReadError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreReadError>()( "DesktopConnectionCatalogStoreReadError", { catalogPath: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to read the desktop connection catalog at ${this.catalogPath}.`; } } export class DesktopConnectionCatalogStoreDocumentDecodeError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreDocumentDecodeError>()( "DesktopConnectionCatalogStoreDocumentDecodeError", { catalogPath: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to decode the desktop connection catalog document at ${this.catalogPath}.`; } } export class DesktopConnectionCatalogStoreMigrationError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreMigrationError>()( "DesktopConnectionCatalogStoreMigrationError", { operation: DesktopConnectionCatalogStoreMigrationOperation, catalogPath: Schema.String, environmentId: Schema.optionalKey(Schema.String), cause: Schema.Defect(), }, ) { override get message(): string { const environment = this.environmentId === undefined ? "" : ` for environment ${this.environmentId}`; return `Legacy desktop saved-environment migration failed during ${this.operation}${environment} into ${this.catalogPath}.`; } } export class DesktopConnectionCatalogStoreProtectionError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreProtectionError>()( "DesktopConnectionCatalogStoreProtectionError", { operation: DesktopConnectionCatalogStoreProtectionOperation, catalogPath: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Desktop connection catalog protection failed during ${this.operation} at ${this.catalogPath}.`; } } export class DesktopConnectionCatalogStore extends Context.Service< DesktopConnectionCatalogStore, { readonly get: Effect.Effect< Option.Option<string>, | DesktopConnectionCatalogStoreReadError | DesktopConnectionCatalogStoreDocumentDecodeError | DesktopConnectionCatalogStoreDecodeError | DesktopConnectionCatalogStoreMigrationError | DesktopConnectionCatalogStoreProtectionError >; readonly set: ( catalog: string, ) => Effect.Effect< boolean, DesktopConnectionCatalogStoreWriteError | DesktopConnectionCatalogStoreProtectionError >; readonly clear: Effect.Effect<void>; } >()("@t3tools/desktop/app/DesktopConnectionCatalogStore") {} function decodeSecretBytes( catalogPath: string, encoded: string, ): Effect.Effect<Uint8Array, DesktopConnectionCatalogStoreDecodeError> { return Effect.fromResult(Encoding.decodeBase64(encoded)).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreDecodeError({ resource: "encryptedCatalog", catalogPath, cause, }), ), ); } const readDocument = ( fileSystem: FileSystem.FileSystem, catalogPath: string, ): Effect.Effect< Option.Option<EncryptedConnectionCatalogDocument>, DesktopConnectionCatalogStoreReadError | DesktopConnectionCatalogStoreDocumentDecodeError > => fileSystem.readFileString(catalogPath).pipe( Effect.catch((error) => error.reason._tag === "NotFound" ? Effect.succeed<string | null>(null) : Effect.fail( new DesktopConnectionCatalogStoreReadError({ catalogPath, cause: error, }), ), ), Effect.flatMap((raw) => raw === null ? Effect.succeed(Option.none<EncryptedConnectionCatalogDocument>()) : decodeEncryptedConnectionCatalogDocumentJson(raw).pipe( Effect.map(Option.some), Effect.mapError( (cause) => new DesktopConnectionCatalogStoreDocumentDecodeError({ catalogPath, cause, }), ), ), ), ); 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<void, DesktopConnectionCatalogStoreWriteError> { const directory = input.path.dirname(input.catalogPath); const tempPath = `${input.catalogPath}.${process.pid}.${input.suffix}.tmp`; const encoded = yield* encodeEncryptedConnectionCatalogDocumentJson(input.document).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreWriteError({ operation: "encode-document", path: input.catalogPath, cause, }), ), ); yield* input.fileSystem.makeDirectory(directory, { recursive: true }).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreWriteError({ operation: "create-directory", path: directory, cause, }), ), ); yield* Effect.gen(function* () { yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreWriteError({ operation: "write-temporary-file", path: tempPath, cause, }), ), ); yield* input.fileSystem.rename(tempPath, input.catalogPath).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreWriteError({ operation: "replace-catalog-file", path: input.catalogPath, cause, }), ), ); }).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.DesktopSavedEnvironments["Service"], catalogPath: string, ): Effect.fn.Return< RuntimeConnectionCatalogDocumentType, DesktopConnectionCatalogStoreMigrationError > { const targets: Array<RuntimeConnectionCatalogDocumentType["targets"][number]> = []; const profiles: Array<RuntimeConnectionCatalogDocumentType["profiles"][number]> = []; const credentials: Array<RuntimeConnectionCatalogDocumentType["credentials"][number]> = []; 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).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreMigrationError({ operation: "read-legacy-secret", catalogPath, environmentId: record.environmentId, cause, }), ), ); if (Option.isSome(token)) { credentials.push({ connectionId: id, credential: new BearerConnectionCredential({ token: token.value }), }); } } return { schemaVersion: 1, targets, profiles, credentials, remoteDpopTokens: [], }; }); export const make = 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 encryptionAvailable = safeStorage.isEncryptionAvailable.pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreProtectionError({ operation: "check-encryption-availability", catalogPath, cause, }), ), ); const writeCatalog = Effect.fn("desktop.connectionCatalogStore.writeCatalog")(function* ( catalog: string, ) { const encryptedCatalog = Encoding.encodeBase64( yield* safeStorage.encryptString(catalog).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreProtectionError({ operation: "encrypt-catalog", catalogPath, cause, }), ), ), ); const suffix = (yield* crypto.randomUUIDv4.pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreWriteError({ operation: "create-temporary-file-name", path: catalogPath, cause, }), ), )).replace(/-/g, ""); yield* writeDocument({ fileSystem, path, catalogPath, document: { version: 1, encryptedCatalog }, suffix, }); }); const migrateLegacyCatalog = Effect.gen(function* () { if (!(yield* encryptionAvailable)) { return Option.none<string>(); } const records = yield* savedEnvironments.getRegistry.pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreMigrationError({ operation: "read-legacy-registry", catalogPath, cause, }), ), ); if (records.length === 0) { return Option.none<string>(); } const catalog = yield* migrateSavedEnvironmentRecords(records, savedEnvironments, catalogPath); const encoded = yield* encodeRuntimeConnectionCatalogDocumentJson(catalog).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreMigrationError({ operation: "encode-catalog", catalogPath, cause, }), ), ); yield* writeCatalog(encoded).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreMigrationError({ operation: "persist-catalog", catalogPath, cause, }), ), ); return Option.some(encoded); }); return DesktopConnectionCatalogStore.of({ get: Effect.gen(function* () { const document = yield* readDocument(fileSystem, catalogPath); if (Option.isNone(document)) { return yield* migrateLegacyCatalog; } if (!(yield* encryptionAvailable)) { return Option.none<string>(); } const decrypted = yield* decodeSecretBytes(catalogPath, document.value.encryptedCatalog).pipe( Effect.flatMap((encryptedCatalog) => safeStorage.decryptString(encryptedCatalog).pipe( Effect.mapError( (cause) => new DesktopConnectionCatalogStoreProtectionError({ operation: "decrypt-catalog", catalogPath, cause, }), ), ), ), ); return Option.some(decrypted); }).pipe(Effect.withSpan("desktop.connectionCatalogStore.get")), set: Effect.fn("desktop.connectionCatalogStore.set")(function* (catalog) { if (!(yield* encryptionAvailable)) { return false; }