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",]);type DesktopConnectionCatalogStoreWriteOperation = typeof DesktopConnectionCatalogStoreWriteOperation.Type; const DesktopConnectionCatalogStoreMigrationOperation = Schema.Literals([ "read-legacy-registry", "read-legacy-secret", "encode-catalog", "persist-catalog",]);type DesktopConnectionCatalogStoreMigrationOperation = typeof DesktopConnectionCatalogStoreMigrationOperation.Type; 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}.`; }} const writeError = ( operation: DesktopConnectionCatalogStoreWriteOperation, path: string, cause: unknown,): DesktopConnectionCatalogStoreWriteError => new DesktopConnectionCatalogStoreWriteError({ operation, path, cause, }); 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}.`; }} const migrationError = ( operation: DesktopConnectionCatalogStoreMigrationOperation, catalogPath: string, cause: unknown, environmentId?: string,): DesktopConnectionCatalogStoreMigrationError => new DesktopConnectionCatalogStoreMigrationError({ operation, catalogPath, ...(environmentId === undefined ? {} : { environmentId }), cause, }); export class DesktopConnectionCatalogStore extends Context.Service< DesktopConnectionCatalogStore, { readonly get: Effect.Effect< Option.Option<string>, | DesktopConnectionCatalogStoreReadError | DesktopConnectionCatalogStoreDocumentDecodeError | DesktopConnectionCatalogStoreDecodeError | DesktopConnectionCatalogStoreMigrationError | ElectronSafeStorage.ElectronSafeStorageError >; readonly set: ( catalog: string, ) => Effect.Effect< boolean, DesktopConnectionCatalogStoreWriteError | ElectronSafeStorage.ElectronSafeStorageError >; 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) => writeError("encode-document", input.catalogPath, cause)), ); yield* input.fileSystem .makeDirectory(directory, { recursive: true }) .pipe(Effect.mapError((cause) => writeError("create-directory", directory, cause))); yield* Effect.gen(function* () { yield* input.fileSystem .writeFileString(tempPath, `${encoded}\n`) .pipe(Effect.mapError((cause) => writeError("write-temporary-file", tempPath, cause))); yield* input.fileSystem .rename(tempPath, input.catalogPath) .pipe( Effect.mapError((cause) => writeError("replace-catalog-file", 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) => migrationError("read-legacy-secret", catalogPath, cause, 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 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 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) => writeError("create-temporary-file-name", catalogPath, cause)), )).replace(/-/g, ""); yield* writeDocument({ fileSystem, path, catalogPath, document: { version: 1, encryptedCatalog }, suffix, }); }); const migrateLegacyCatalog = Effect.gen(function* () { if (!(yield* safeStorage.isEncryptionAvailable)) { return Option.none<string>(); } const records = yield* savedEnvironments.getRegistry.pipe( Effect.mapError((cause) => migrationError("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) => migrationError("encode-catalog", catalogPath, cause)), ); yield* writeCatalog(encoded).pipe( Effect.mapError((cause) => migrationError("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* safeStorage.isEncryptionAvailable)) { return Option.none<string>(); } const decrypted = yield* decodeSecretBytes(catalogPath, 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"), ), });}); export const layer = Layer.effect(DesktopConnectionCatalogStore, make);
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",
]);
type DesktopConnectionCatalogStoreWriteOperation =
typeof DesktopConnectionCatalogStoreWriteOperation.Type;
const DesktopConnectionCatalogStoreMigrationOperation = Schema.Literals([
"read-legacy-registry",
"read-legacy-secret",
"encode-catalog",
"persist-catalog",
]);
type DesktopConnectionCatalogStoreMigrationOperation =
typeof DesktopConnectionCatalogStoreMigrationOperation.Type;
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}.`;
}
}
const writeError = (
operation: DesktopConnectionCatalogStoreWriteOperation,
path: string,
cause: unknown,
): DesktopConnectionCatalogStoreWriteError =>
new DesktopConnectionCatalogStoreWriteError({
operation,
path,
cause,
});
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}.`;
}
}
const migrationError = (
operation: DesktopConnectionCatalogStoreMigrationOperation,
catalogPath: string,
cause: unknown,
environmentId?: string,
): DesktopConnectionCatalogStoreMigrationError =>
new DesktopConnectionCatalogStoreMigrationError({
operation,
catalogPath,
...(environmentId === undefined ? {} : { environmentId }),
cause,
});
export class DesktopConnectionCatalogStore extends Context.Service<
DesktopConnectionCatalogStore,
{
readonly get: Effect.Effect<
Option.Option<string>,
| DesktopConnectionCatalogStoreReadError
| DesktopConnectionCatalogStoreDocumentDecodeError
| DesktopConnectionCatalogStoreDecodeError
| DesktopConnectionCatalogStoreMigrationError
| ElectronSafeStorage.ElectronSafeStorageError
>;
readonly set: (
catalog: string,
) => Effect.Effect<
boolean,
DesktopConnectionCatalogStoreWriteError | ElectronSafeStorage.ElectronSafeStorageError
>;
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) => writeError("encode-document", input.catalogPath, cause)),
);
yield* input.fileSystem
.makeDirectory(directory, { recursive: true })
.pipe(Effect.mapError((cause) => writeError("create-directory", directory, cause)));
yield* Effect.gen(function* () {
yield* input.fileSystem
.writeFileString(tempPath, `${encoded}\n`)
.pipe(Effect.mapError((cause) => writeError("write-temporary-file", tempPath, cause)));
yield* input.fileSystem
.rename(tempPath, input.catalogPath)
.pipe(
Effect.mapError((cause) => writeError("replace-catalog-file", 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) =>
migrationError("read-legacy-secret", catalogPath, cause, 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 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 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) => writeError("create-temporary-file-name", catalogPath, cause)),
)).replace(/-/g, "");
yield* writeDocument({
fileSystem,
path,
catalogPath,
document: { version: 1, encryptedCatalog },
suffix,
});
});
const migrateLegacyCatalog = Effect.gen(function* () {
if (!(yield* safeStorage.isEncryptionAvailable)) {
return Option.none<string>();
}
const records = yield* savedEnvironments.getRegistry.pipe(
Effect.mapError((cause) => migrationError("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) => migrationError("encode-catalog", catalogPath, cause)),
);
yield* writeCatalog(encoded).pipe(
Effect.mapError((cause) => migrationError("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* safeStorage.isEncryptionAvailable)) {
return Option.none<string>();
}
const decrypted = yield* decodeSecretBytes(catalogPath, 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"),
),
});
});
export const layer = Layer.effect(DesktopConnectionCatalogStore, make);