1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, describe, it } from "@effect/vitest";
3import { ConnectionCatalogDocument } from "@t3tools/client-runtime/platform";
4import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts";
5import * as Effect from "effect/Effect";
6import * as Base64 from "effect/encoding/Base64";
7import * as FileSystem from "effect/FileSystem";
8import * as Path from "effect/Path";
9import * as Layer from "effect/Layer";
10import * as Option from "effect/Option";
11import * as PlatformError from "effect/PlatformError";
12import * as Ref from "effect/Ref";
13import * as Schema from "effect/Schema";
14
15import * as ElectronSafeStorage from "../electron/ElectronSafeStorage.ts";
16import * as DesktopSavedEnvironments from "../settings/DesktopSavedEnvironments.ts";
17import * as DesktopConfig from "./DesktopConfig.ts";
18import * as DesktopConnectionCatalogStore from "./DesktopConnectionCatalogStore.ts";
19import * as DesktopEnvironment from "./DesktopEnvironment.ts";
20
21const textDecoder = new TextDecoder();
22const textEncoder = new TextEncoder();
23const decodeConnectionCatalog = Schema.decodeEffect(
24 Schema.fromJsonString(ConnectionCatalogDocument),
25);
26const encodeLegacySavedEnvironments = Schema.encodeEffect(
27 Schema.fromJsonString(
28 Schema.Struct({ version: Schema.Literal(1), records: Schema.Array(Schema.Unknown) }),
29 ),
30);
31function layerSafeStorageFor(available: boolean, failDecrypt: Ref.Ref<boolean> | null = null) {
32 return Layer.succeed(ElectronSafeStorage.ElectronSafeStorage, {
33 isEncryptionAvailable: Effect.succeed(available),
34 encryptString: (value) => Effect.succeed(textEncoder.encode(`encrypted:${value}`)),
35 decryptString: (value) => {
36 return Effect.gen(function* () {
37 const decoded = textDecoder.decode(value);
38 if (
39 !decoded.startsWith("encrypted:") ||
40 (failDecrypt !== null && (yield* Ref.get(failDecrypt)))
41 ) {
42 return yield* new ElectronSafeStorage.ElectronSafeStorageDecryptError({
43 cause: new Error("invalid encrypted catalog"),
44 });
45 }
46 return decoded.slice("encrypted:".length);
47 });
48 },
49 selectedStorageBackend: Effect.succeedNone,
50 } satisfies ElectronSafeStorage.ElectronSafeStorage["Service"]);
51}
52
53function layerFor(
54 baseDir: string,
55 encryptionAvailable = true,
56 failDecrypt: Ref.Ref<boolean> | null = null,
57 fileSystemLayer: Layer.Layer<FileSystem.FileSystem> = NodeServices.layer,
58) {
59 const layerEnvironment = DesktopEnvironment.layer({
60 dirname: "/repo/apps/desktop/src",
61 homeDirectory: baseDir,
62 platform: "darwin",
63 processArch: "arm64",
64 appVersion: "1.2.3",
65 appPath: "/repo",
66 isPackaged: true,
67 resourcesPath: "/missing/resources",
68 runningUnderArm64Translation: false,
69 }).pipe(
70 Layer.provide(
71 Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: baseDir })),
72 ),
73 );
74 const layerSafeStorage = layerSafeStorageFor(encryptionAvailable, failDecrypt);
75 const layerDependencies = Layer.mergeAll(
76 layerEnvironment,
77 layerSafeStorage,
78 NodeServices.layer,
79 fileSystemLayer,
80 );
81 const layerSavedEnvironments = DesktopSavedEnvironments.layer.pipe(
82 Layer.provideMerge(layerDependencies),
83 );
84
85 return DesktopConnectionCatalogStore.layer.pipe(
86 Layer.provideMerge(layerSavedEnvironments),
87 Layer.provideMerge(layerDependencies),
88 );
89}
90
91const withStore = <A, E, R>(
92 effect: Effect.Effect<A, E, R | DesktopConnectionCatalogStore.DesktopConnectionCatalogStore>,
93 encryptionAvailable = true,
94) =>
95 Effect.gen(function* () {
96 const fileSystem = yield* FileSystem.FileSystem;
97 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
98 prefix: "t3-desktop-connection-catalog-test-",
99 });
100 return yield* effect.pipe(Effect.provide(layerFor(baseDir, encryptionAvailable)));
101 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped);
102
103describe("DesktopConnectionCatalogStore", () => {
104 it.effect("persists, reads, and clears an encrypted connection catalog", () =>
105 withStore(
106 Effect.gen(function* () {
107 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
108 const catalog = '{"schemaVersion":1,"targets":[]}';
109
110 assert.isTrue(yield* store.set(catalog));
111 assert.deepStrictEqual(yield* store.get, Option.some(catalog));
112
113 yield* store.clear;
114 assert.deepStrictEqual(yield* store.get, Option.none());
115 }),
116 ),
117 );
118
119 it.effect("does not persist when secure storage is unavailable", () =>
120 withStore(
121 Effect.gen(function* () {
122 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
123 assert.isFalse(yield* store.set("{}"));
124 assert.deepStrictEqual(yield* store.get, Option.none());
125 }),
126 false,
127 ),
128 );
129
130 it.effect("migrates legacy relay, SSH, bearer profile, and credential data", () =>
131 withStore(
132 Effect.gen(function* () {
133 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
134 const environment = yield* DesktopEnvironment.DesktopEnvironment;
135 const fileSystem = yield* FileSystem.FileSystem;
136 const records: readonly PersistedSavedEnvironmentRecord[] = [
137 {
138 environmentId: EnvironmentId.make("relay-environment"),
139 label: "Relay",
140 httpBaseUrl: "https://relay.example.com/",
141 wsBaseUrl: "wss://relay.example.com/",
142 createdAt: "2026-06-01T00:00:00.000Z",
143 lastConnectedAt: null,
144 relayManaged: { relayUrl: "https://relay-control.example.com/" },
145 },
146 {
147 environmentId: EnvironmentId.make("ssh-environment"),
148 label: "SSH",
149 httpBaseUrl: "http://127.0.0.1:41773/",
150 wsBaseUrl: "ws://127.0.0.1:41773/",
151 createdAt: "2026-06-02T00:00:00.000Z",
152 lastConnectedAt: null,
153 desktopSsh: {
154 alias: "devbox",
155 hostname: "devbox.example.com",
156 username: "julius",
157 port: 22,
158 },
159 },
160 {
161 environmentId: EnvironmentId.make("bearer-environment"),
162 label: "Bearer",
163 httpBaseUrl: "https://bearer.example.com/",
164 wsBaseUrl: "wss://bearer.example.com/",
165 createdAt: "2026-06-03T00:00:00.000Z",
166 lastConnectedAt: null,
167 },
168 ];
169 yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
170 yield* fileSystem.writeFileString(
171 environment.savedEnvironmentRegistryPath,
172 yield* encodeLegacySavedEnvironments({
173 version: 1,
174 records: records.map((record) =>
175 record.environmentId === "bearer-environment"
176 ? {
177 ...record,
178 encryptedBearerToken: Base64.encode(
179 textEncoder.encode("encrypted:legacy-token"),
180 ),
181 }
182 : record,
183 ),
184 }),
185 );
186
187 const migrated = yield* store.get;
188 assert.isTrue(Option.isSome(migrated));
189 if (Option.isNone(migrated)) {
190 return;
191 }
192 const catalog = yield* decodeConnectionCatalog(migrated.value);
193
194 assert.deepInclude(catalog.targets[0], {
195 _tag: "RelayConnectionTarget",
196 environmentId: EnvironmentId.make("relay-environment"),
197 label: "Relay",
198 });
199 assert.deepInclude(catalog.targets[1], {
200 _tag: "SshConnectionTarget",
201 environmentId: EnvironmentId.make("ssh-environment"),
202 label: "SSH",
203 connectionId: "ssh:ssh-environment",
204 });
205 assert.deepInclude(catalog.targets[2], {
206 _tag: "BearerConnectionTarget",
207 environmentId: EnvironmentId.make("bearer-environment"),
208 label: "Bearer",
209 connectionId: "bearer:bearer-environment",
210 });
211 assert.deepInclude(catalog.profiles[0], {
212 _tag: "SshConnectionProfile",
213 connectionId: "ssh:ssh-environment",
214 environmentId: EnvironmentId.make("ssh-environment"),
215 label: "SSH",
216 target: {
217 alias: "devbox",
218 hostname: "devbox.example.com",
219 username: "julius",
220 port: 22,
221 },
222 });
223 assert.deepInclude(catalog.profiles[1], {
224 _tag: "BearerConnectionProfile",
225 connectionId: "bearer:bearer-environment",
226 environmentId: EnvironmentId.make("bearer-environment"),
227 label: "Bearer",
228 httpBaseUrl: "https://bearer.example.com/",
229 wsBaseUrl: "wss://bearer.example.com/",
230 });
231 assert.equal(catalog.credentials.length, 1);
232 assert.equal(catalog.credentials[0]?.connectionId, "bearer:bearer-environment");
233 assert.equal(catalog.credentials[0]?.credential._tag, "BearerConnectionCredential");
234 if (catalog.credentials[0]?.credential._tag === "BearerConnectionCredential") {
235 assert.equal(catalog.credentials[0].credential.token, "legacy-token");
236 }
237
238 yield* fileSystem.writeFileString(
239 environment.savedEnvironmentRegistryPath,
240 '{"version":1,"records":[]}',
241 );
242 assert.deepEqual(yield* store.get, migrated);
243 }),
244 ),
245 );
246
247 it.effect("surfaces malformed catalog documents without deleting them", () =>
248 withStore(
249 Effect.gen(function* () {
250 const path = yield* Path.Path;
251 const environment = yield* DesktopEnvironment.DesktopEnvironment;
252 const fileSystem = yield* FileSystem.FileSystem;
253 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
254 const catalogPath = path.join(environment.stateDir, "connection-catalog.json");
255 yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
256 yield* fileSystem.writeFileString(catalogPath, "{not-json");
257
258 const error = yield* store.get.pipe(Effect.flip);
259 assert.instanceOf(
260 error,
261 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreDocumentDecodeError,
262 );
263 assert.equal(error.catalogPath, catalogPath);
264 assert.exists(error.cause);
265 assert.equal(yield* fileSystem.readFileString(catalogPath), "{not-json");
266 }),
267 ),
268 );
269
270 it.effect("surfaces catalog filesystem failures instead of treating them as missing", () =>
271 Effect.gen(function* () {
272 const path = yield* Path.Path;
273 const baseFileSystem = yield* FileSystem.FileSystem;
274 const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({
275 prefix: "t3-desktop-connection-catalog-test-",
276 });
277 const permissionError = PlatformError.systemError({
278 _tag: "PermissionDenied",
279 module: "FileSystem",
280 method: "readFileString",
281 pathOrDescriptor: path.join(baseDir, "userdata", "connection-catalog.json"),
282 });
283 const layerFileSystem = Layer.succeed(
284 FileSystem.FileSystem,
285 FileSystem.makeNoop({
286 readFileString: () => Effect.fail(permissionError),
287 }),
288 );
289 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore.pipe(
290 Effect.provide(layerFor(baseDir, true, null, layerFileSystem)),
291 );
292
293 const error = yield* store.get.pipe(Effect.flip);
294 assert.instanceOf(
295 error,
296 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreReadError,
297 );
298 assert.equal(error.catalogPath, path.join(baseDir, "userdata", "connection-catalog.json"));
299 assert.strictEqual(error.cause, permissionError);
300 assert.equal(
301 error.message,
302 `Failed to read the desktop connection catalog at ${path.join(baseDir, "userdata", "connection-catalog.json")}.`,
303 );
304 assert.notEqual(error.message, permissionError.message);
305 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
306 );
307
308 it.effect("reports the failed catalog write operation and path", () =>
309 Effect.gen(function* () {
310 const baseFileSystem = yield* FileSystem.FileSystem;
311 const path = yield* Path.Path;
312 const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({
313 prefix: "t3-desktop-connection-catalog-test-",
314 });
315 const permissionError = PlatformError.systemError({
316 _tag: "PermissionDenied",
317 module: "FileSystem",
318 method: "makeDirectory",
319 pathOrDescriptor: path.join(baseDir, "userdata"),
320 });
321 const layerFileSystem = Layer.succeed(
322 FileSystem.FileSystem,
323 FileSystem.makeNoop({
324 makeDirectory: () => Effect.fail(permissionError),
325 }),
326 );
327 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore.pipe(
328 Effect.provide(layerFor(baseDir, true, null, layerFileSystem)),
329 );
330
331 const error = yield* store.set("{}").pipe(Effect.flip);
332 assert.instanceOf(
333 error,
334 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreWriteError,
335 );
336 assert.equal(error.operation, "create-directory");
337 assert.equal(error.path, path.join(baseDir, "userdata"));
338 assert.strictEqual(error.cause, permissionError);
339 assert.equal(
340 error.message,
341 `Desktop connection catalog write failed during create-directory at ${path.join(baseDir, "userdata")}.`,
342 );
343 assert.notEqual(error.message, permissionError.message);
344 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
345 );
346
347 it.effect("reports the legacy migration stage", () =>
348 withStore(
349 Effect.gen(function* () {
350 const path = yield* Path.Path;
351 const environment = yield* DesktopEnvironment.DesktopEnvironment;
352 const fileSystem = yield* FileSystem.FileSystem;
353 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
354 yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
355 yield* fileSystem.writeFileString(environment.savedEnvironmentRegistryPath, "{not-json");
356
357 const error = yield* store.get.pipe(Effect.flip);
358 assert.instanceOf(
359 error,
360 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreMigrationError,
361 );
362 assert.equal(error.operation, "read-legacy-registry");
363 assert.equal(error.catalogPath, path.join(environment.stateDir, "connection-catalog.json"));
364 assert.instanceOf(
365 error.cause,
366 DesktopSavedEnvironments.DesktopSavedEnvironmentsDocumentDecodeError,
367 );
368 const registryError =
369 error.cause as DesktopSavedEnvironments.DesktopSavedEnvironmentsDocumentDecodeError;
370 assert.exists(registryError.cause);
371 assert.equal(
372 error.message,
373 `Legacy desktop saved-environment migration failed during read-legacy-registry into ${path.join(environment.stateDir, "connection-catalog.json")}.`,
374 );
375 assert.notEqual(error.message, registryError.message);
376 }),
377 ),
378 );
379
380 it.effect("reports invalid encrypted catalog data without exposing it", () =>
381 withStore(
382 Effect.gen(function* () {
383 const path = yield* Path.Path;
384 const environment = yield* DesktopEnvironment.DesktopEnvironment;
385 const fileSystem = yield* FileSystem.FileSystem;
386 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
387 const catalogPath = path.join(environment.stateDir, "connection-catalog.json");
388 yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
389 yield* fileSystem.writeFileString(catalogPath, '{"version":1,"encryptedCatalog":"%%%"}\n');
390
391 const error = yield* store.get.pipe(Effect.flip);
392 assert.instanceOf(
393 error,
394 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreDecodeError,
395 );
396 assert.equal(error.resource, "encryptedCatalog");
397 assert.equal(error.catalogPath, catalogPath);
398 assert.exists(error.cause);
399 assert.equal(
400 error.message,
401 `Failed to decode encryptedCatalog for the desktop connection catalog at ${catalogPath}.`,
402 );
403 assert.notInclude(error.message, "%%%");
404 }),
405 ),
406 );
407
408 it.effect("surfaces a catalog that can no longer be decrypted without deleting it", () =>
409 Effect.gen(function* () {
410 const path = yield* Path.Path;
411 const fileSystem = yield* FileSystem.FileSystem;
412 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
413 prefix: "t3-desktop-connection-catalog-test-",
414 });
415 const failDecrypt = yield* Ref.make(false);
416 const layer = layerFor(baseDir, true, failDecrypt);
417 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore.pipe(
418 Effect.provide(layer),
419 );
420
421 assert.isTrue(yield* store.set('{"schemaVersion":1,"targets":[]}'));
422 yield* Ref.set(failDecrypt, true);
423 const error = yield* store.get.pipe(Effect.flip);
424 assert.instanceOf(
425 error,
426 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreProtectionError,
427 );
428 assert.equal(error.operation, "decrypt-catalog");
429 assert.equal(error.catalogPath, path.join(baseDir, "userdata", "connection-catalog.json"));
430 assert.instanceOf(error.cause, ElectronSafeStorage.ElectronSafeStorageDecryptError);
431 const decryptError = error.cause as ElectronSafeStorage.ElectronSafeStorageDecryptError;
432 assert.instanceOf(decryptError.cause, Error);
433 assert.equal(decryptError.cause.message, "invalid encrypted catalog");
434 assert.equal(
435 error.message,
436 `Desktop connection catalog protection failed during decrypt-catalog at ${path.join(baseDir, "userdata", "connection-catalog.json")}.`,
437 );
438 assert.notEqual(error.message, decryptError.message);
439 yield* Ref.set(failDecrypt, false);
440 assert.deepStrictEqual(yield* store.get, Option.some('{"schemaVersion":1,"targets":[]}'));
441 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
442 );
443});