authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 12:28:14-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 12:28:14-07:00
logc1d8a22f073e36768a8af5b0ef6472d802cbfa4d
treef04ccd13c94d7efe1cdd49043a37ea44c7b32f2c
parent7b791895ad043e180c1d56595a08866e16d0766e
signature Signed by PGP key B5690EEEBB952194

[codex] Structure desktop persisted credential errors (#3239)

Co-authored-by: codex <codex@users.noreply.github.com>

4 files changed, 291 insertions(+), 93 deletions(-)

apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts+15-1
......@@ -393,7 +393,21 @@ describe("DesktopConnectionCatalogStore", () => {
393393 assert.isTrue(yield* store.set('{"schemaVersion":1,"targets":[]}'));
394394 yield* Ref.set(failDecrypt, true);
395395 const error = yield* store.get.pipe(Effect.flip);
396 assert.instanceOf(error, ElectronSafeStorage.ElectronSafeStorageDecryptError);
396 assert.instanceOf(
397 error,
398 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreProtectionError,
399 );
400 assert.equal(error.operation, "decrypt-catalog");
401 assert.equal(error.catalogPath, `${baseDir}/userdata/connection-catalog.json`);
402 assert.instanceOf(error.cause, ElectronSafeStorage.ElectronSafeStorageDecryptError);
403 const decryptError = error.cause as ElectronSafeStorage.ElectronSafeStorageDecryptError;
404 assert.instanceOf(decryptError.cause, Error);
405 assert.equal(decryptError.cause.message, "invalid encrypted catalog");
406 assert.equal(
407 error.message,
408 `Desktop connection catalog protection failed during decrypt-catalog at ${baseDir}/userdata/connection-catalog.json.`,
409 );
410 assert.notEqual(error.message, decryptError.message);
397411 yield* Ref.set(failDecrypt, false);
398412 assert.deepStrictEqual(yield* store.get, Option.some('{"schemaVersion":1,"targets":[]}'));
399413 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
apps/desktop/src/app/DesktopConnectionCatalogStore.ts+138-57
......@@ -53,8 +53,6 @@ const DesktopConnectionCatalogStoreWriteOperation = Schema.Literals([
5353 "write-temporary-file",
5454 "replace-catalog-file",
5555]);
56type DesktopConnectionCatalogStoreWriteOperation =
57 typeof DesktopConnectionCatalogStoreWriteOperation.Type;
5856
5957const DesktopConnectionCatalogStoreMigrationOperation = Schema.Literals([
6058 "read-legacy-registry",
......@@ -62,8 +60,12 @@ const DesktopConnectionCatalogStoreMigrationOperation = Schema.Literals([
6260 "encode-catalog",
6361 "persist-catalog",
6462]);
65type DesktopConnectionCatalogStoreMigrationOperation =
66 typeof DesktopConnectionCatalogStoreMigrationOperation.Type;
63
64const DesktopConnectionCatalogStoreProtectionOperation = Schema.Literals([
65 "check-encryption-availability",
66 "encrypt-catalog",
67 "decrypt-catalog",
68]);
6769
6870export class DesktopConnectionCatalogStoreWriteError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreWriteError>()(
6971 "DesktopConnectionCatalogStoreWriteError",
......@@ -78,17 +80,6 @@ export class DesktopConnectionCatalogStoreWriteError extends Schema.TaggedErrorC
7880 }
7981}
8082
81const writeError = (
82 operation: DesktopConnectionCatalogStoreWriteOperation,
83 path: string,
84 cause: unknown,
85): DesktopConnectionCatalogStoreWriteError =>
86 new DesktopConnectionCatalogStoreWriteError({
87 operation,
88 path,
89 cause,
90 });
91
9283export class DesktopConnectionCatalogStoreDecodeError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreDecodeError>()(
9384 "DesktopConnectionCatalogStoreDecodeError",
9485 {
......@@ -142,18 +133,18 @@ export class DesktopConnectionCatalogStoreMigrationError extends Schema.TaggedEr
142133 }
143134}
144135
145const migrationError = (
146 operation: DesktopConnectionCatalogStoreMigrationOperation,
147 catalogPath: string,
148 cause: unknown,
149 environmentId?: string,
150): DesktopConnectionCatalogStoreMigrationError =>
151 new DesktopConnectionCatalogStoreMigrationError({
152 operation,
153 catalogPath,
154 ...(environmentId === undefined ? {} : { environmentId }),
155 cause,
156 });
136export class DesktopConnectionCatalogStoreProtectionError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreProtectionError>()(
137 "DesktopConnectionCatalogStoreProtectionError",
138 {
139 operation: DesktopConnectionCatalogStoreProtectionOperation,
140 catalogPath: Schema.String,
141 cause: Schema.Defect(),
142 },
143) {
144 override get message(): string {
145 return `Desktop connection catalog protection failed during ${this.operation} at ${this.catalogPath}.`;
146 }
147}
157148
158149export class DesktopConnectionCatalogStore extends Context.Service<
159150 DesktopConnectionCatalogStore,
......@@ -164,13 +155,13 @@ export class DesktopConnectionCatalogStore extends Context.Service<
164155 | DesktopConnectionCatalogStoreDocumentDecodeError
165156 | DesktopConnectionCatalogStoreDecodeError
166157 | DesktopConnectionCatalogStoreMigrationError
167 | ElectronSafeStorage.ElectronSafeStorageError
158 | DesktopConnectionCatalogStoreProtectionError
168159 >;
169160 readonly set: (
170161 catalog: string,
171162 ) => Effect.Effect<
172163 boolean,
173 DesktopConnectionCatalogStoreWriteError | ElectronSafeStorage.ElectronSafeStorageError
164 DesktopConnectionCatalogStoreWriteError | DesktopConnectionCatalogStoreProtectionError
174165 >;
175166 readonly clear: Effect.Effect<void>;
176167 }
......@@ -236,20 +227,46 @@ const writeDocument = Effect.fn("desktop.connectionCatalogStore.writeDocument")(
236227 const directory = input.path.dirname(input.catalogPath);
237228 const tempPath = `${input.catalogPath}.${process.pid}.${input.suffix}.tmp`;
238229 const encoded = yield* encodeEncryptedConnectionCatalogDocumentJson(input.document).pipe(
239 Effect.mapError((cause) => writeError("encode-document", input.catalogPath, cause)),
230 Effect.mapError(
231 (cause) =>
232 new DesktopConnectionCatalogStoreWriteError({
233 operation: "encode-document",
234 path: input.catalogPath,
235 cause,
236 }),
237 ),
238 );
239 yield* input.fileSystem.makeDirectory(directory, { recursive: true }).pipe(
240 Effect.mapError(
241 (cause) =>
242 new DesktopConnectionCatalogStoreWriteError({
243 operation: "create-directory",
244 path: directory,
245 cause,
246 }),
247 ),
240248 );
241 yield* input.fileSystem
242 .makeDirectory(directory, { recursive: true })
243 .pipe(Effect.mapError((cause) => writeError("create-directory", directory, cause)));
244249 yield* Effect.gen(function* () {
245 yield* input.fileSystem
246 .writeFileString(tempPath, `${encoded}\n`)
247 .pipe(Effect.mapError((cause) => writeError("write-temporary-file", tempPath, cause)));
248 yield* input.fileSystem
249 .rename(tempPath, input.catalogPath)
250 .pipe(
251 Effect.mapError((cause) => writeError("replace-catalog-file", input.catalogPath, cause)),
252 );
250 yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`).pipe(
251 Effect.mapError(
252 (cause) =>
253 new DesktopConnectionCatalogStoreWriteError({
254 operation: "write-temporary-file",
255 path: tempPath,
256 cause,
257 }),
258 ),
259 );
260 yield* input.fileSystem.rename(tempPath, input.catalogPath).pipe(
261 Effect.mapError(
262 (cause) =>
263 new DesktopConnectionCatalogStoreWriteError({
264 operation: "replace-catalog-file",
265 path: input.catalogPath,
266 cause,
267 }),
268 ),
269 );
253270 }).pipe(
254271 Effect.ensuring(
255272 input.fileSystem.remove(tempPath, { force: true }).pipe(
......@@ -330,13 +347,17 @@ const migrateSavedEnvironmentRecords = Effect.fn(
330347 wsBaseUrl: record.wsBaseUrl,
331348 }),
332349 );
333 const token = yield* savedEnvironments
334 .getSecret(record.environmentId)
335 .pipe(
336 Effect.mapError((cause) =>
337 migrationError("read-legacy-secret", catalogPath, cause, record.environmentId),
338 ),
339 );
350 const token = yield* savedEnvironments.getSecret(record.environmentId).pipe(
351 Effect.mapError(
352 (cause) =>
353 new DesktopConnectionCatalogStoreMigrationError({
354 operation: "read-legacy-secret",
355 catalogPath,
356 environmentId: record.environmentId,
357 cause,
358 }),
359 ),
360 );
340361 if (Option.isSome(token)) {
341362 credentials.push({
342363 connectionId: id,
......@@ -362,13 +383,41 @@ export const make = Effect.gen(function* () {
362383 const crypto = yield* Crypto.Crypto;
363384 const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments;
364385 const catalogPath = path.join(environment.stateDir, "connection-catalog.json");
386 const encryptionAvailable = safeStorage.isEncryptionAvailable.pipe(
387 Effect.mapError(
388 (cause) =>
389 new DesktopConnectionCatalogStoreProtectionError({
390 operation: "check-encryption-availability",
391 catalogPath,
392 cause,
393 }),
394 ),
395 );
365396
366397 const writeCatalog = Effect.fn("desktop.connectionCatalogStore.writeCatalog")(function* (
367398 catalog: string,
368399 ) {
369 const encryptedCatalog = Encoding.encodeBase64(yield* safeStorage.encryptString(catalog));
400 const encryptedCatalog = Encoding.encodeBase64(
401 yield* safeStorage.encryptString(catalog).pipe(
402 Effect.mapError(
403 (cause) =>
404 new DesktopConnectionCatalogStoreProtectionError({
405 operation: "encrypt-catalog",
406 catalogPath,
407 cause,
408 }),
409 ),
410 ),
411 );
370412 const suffix = (yield* crypto.randomUUIDv4.pipe(
371 Effect.mapError((cause) => writeError("create-temporary-file-name", catalogPath, cause)),
413 Effect.mapError(
414 (cause) =>
415 new DesktopConnectionCatalogStoreWriteError({
416 operation: "create-temporary-file-name",
417 path: catalogPath,
418 cause,
419 }),
420 ),
372421 )).replace(/-/g, "");
373422 yield* writeDocument({
374423 fileSystem,
......@@ -380,21 +429,42 @@ export const make = Effect.gen(function* () {
380429 });
381430
382431 const migrateLegacyCatalog = Effect.gen(function* () {
383 if (!(yield* safeStorage.isEncryptionAvailable)) {
432 if (!(yield* encryptionAvailable)) {
384433 return Option.none<string>();
385434 }
386435 const records = yield* savedEnvironments.getRegistry.pipe(
387 Effect.mapError((cause) => migrationError("read-legacy-registry", catalogPath, cause)),
436 Effect.mapError(
437 (cause) =>
438 new DesktopConnectionCatalogStoreMigrationError({
439 operation: "read-legacy-registry",
440 catalogPath,
441 cause,
442 }),
443 ),
388444 );
389445 if (records.length === 0) {
390446 return Option.none<string>();
391447 }
392448 const catalog = yield* migrateSavedEnvironmentRecords(records, savedEnvironments, catalogPath);
393449 const encoded = yield* encodeRuntimeConnectionCatalogDocumentJson(catalog).pipe(
394 Effect.mapError((cause) => migrationError("encode-catalog", catalogPath, cause)),
450 Effect.mapError(
451 (cause) =>
452 new DesktopConnectionCatalogStoreMigrationError({
453 operation: "encode-catalog",
454 catalogPath,
455 cause,
456 }),
457 ),
395458 );
396459 yield* writeCatalog(encoded).pipe(
397 Effect.mapError((cause) => migrationError("persist-catalog", catalogPath, cause)),
460 Effect.mapError(
461 (cause) =>
462 new DesktopConnectionCatalogStoreMigrationError({
463 operation: "persist-catalog",
464 catalogPath,
465 cause,
466 }),
467 ),
398468 );
399469 return Option.some(encoded);
400470 });
......@@ -405,16 +475,27 @@ export const make = Effect.gen(function* () {
405475 if (Option.isNone(document)) {
406476 return yield* migrateLegacyCatalog;
407477 }
408 if (!(yield* safeStorage.isEncryptionAvailable)) {
478 if (!(yield* encryptionAvailable)) {
409479 return Option.none<string>();
410480 }
411481 const decrypted = yield* decodeSecretBytes(catalogPath, document.value.encryptedCatalog).pipe(
412 Effect.flatMap(safeStorage.decryptString),
482 Effect.flatMap((encryptedCatalog) =>
483 safeStorage.decryptString(encryptedCatalog).pipe(
484 Effect.mapError(
485 (cause) =>
486 new DesktopConnectionCatalogStoreProtectionError({
487 operation: "decrypt-catalog",
488 catalogPath,
489 cause,
490 }),
491 ),
492 ),
493 ),
413494 );
414495 return Option.some(decrypted);
415496 }).pipe(Effect.withSpan("desktop.connectionCatalogStore.get")),
416497 set: Effect.fn("desktop.connectionCatalogStore.set")(function* (catalog) {
417 if (!(yield* safeStorage.isEncryptionAvailable)) {
498 if (!(yield* encryptionAvailable)) {
418499 return false;
419500 }
420501 yield* writeCatalog(catalog);
apps/desktop/src/settings/DesktopSavedEnvironments.test.ts+18-3
......@@ -271,10 +271,11 @@ describe("DesktopSavedEnvironments", () => {
271271 ),
272272 );
273273
274 it.effect("surfaces typed safe storage availability failures", () => {
274 it.effect("adds saved-environment context to safe storage availability failures", () => {
275275 const cause = new Error("safe storage unavailable");
276276 return withSavedEnvironments(
277277 Effect.gen(function* () {
278 const environment = yield* DesktopEnvironment.DesktopEnvironment;
278279 const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments;
279280 yield* savedEnvironments.setRegistry([savedRegistryRecord]);
280281
......@@ -285,8 +286,22 @@ describe("DesktopSavedEnvironments", () => {
285286 })
286287 .pipe(Effect.flip);
287288
288 assert.instanceOf(error, ElectronSafeStorage.ElectronSafeStorageAvailabilityError);
289 assert.equal(error.cause, cause);
289 assert.instanceOf(
290 error,
291 DesktopSavedEnvironments.DesktopSavedEnvironmentSecretProtectionError,
292 );
293 assert.equal(error.operation, "check-encryption-availability");
294 assert.equal(error.environmentId, savedRegistryRecord.environmentId);
295 assert.equal(error.registryPath, environment.savedEnvironmentRegistryPath);
296 assert.instanceOf(error.cause, ElectronSafeStorage.ElectronSafeStorageAvailabilityError);
297 const availabilityError =
298 error.cause as ElectronSafeStorage.ElectronSafeStorageAvailabilityError;
299 assert.strictEqual(availabilityError.cause, cause);
300 assert.equal(
301 error.message,
302 `Desktop saved-environment secret protection failed during check-encryption-availability for environment ${savedRegistryRecord.environmentId} at ${environment.savedEnvironmentRegistryPath}.`,
303 );
304 assert.notEqual(error.message, availabilityError.message);
290305 }),
291306 { availabilityError: cause },
292307 );
apps/desktop/src/settings/DesktopSavedEnvironments.ts+120-32
......@@ -77,7 +77,12 @@ const DesktopSavedEnvironmentsWriteOperation = Schema.Literals([
7777 "write-temporary-file",
7878 "replace-registry-file",
7979]);
80type DesktopSavedEnvironmentsWriteOperation = typeof DesktopSavedEnvironmentsWriteOperation.Type;
80
81const DesktopSavedEnvironmentSecretProtectionOperation = Schema.Literals([
82 "check-encryption-availability",
83 "encrypt-secret",
84 "decrypt-secret",
85]);
8186
8287export class DesktopSavedEnvironmentsWriteError extends Schema.TaggedErrorClass<DesktopSavedEnvironmentsWriteError>()(
8388 "DesktopSavedEnvironmentsWriteError",
......@@ -92,17 +97,6 @@ export class DesktopSavedEnvironmentsWriteError extends Schema.TaggedErrorClass<
9297 }
9398}
9499
95const writeError = (
96 operation: DesktopSavedEnvironmentsWriteOperation,
97 path: string,
98 cause: unknown,
99): DesktopSavedEnvironmentsWriteError =>
100 new DesktopSavedEnvironmentsWriteError({
101 operation,
102 path,
103 cause,
104 });
105
106100export class DesktopSavedEnvironmentsReadError extends Schema.TaggedErrorClass<DesktopSavedEnvironmentsReadError>()(
107101 "DesktopSavedEnvironmentsReadError",
108102 {
......@@ -141,6 +135,20 @@ export class DesktopSavedEnvironmentSecretDecodeError extends Schema.TaggedError
141135 }
142136}
143137
138export class DesktopSavedEnvironmentSecretProtectionError extends Schema.TaggedErrorClass<DesktopSavedEnvironmentSecretProtectionError>()(
139 "DesktopSavedEnvironmentSecretProtectionError",
140 {
141 operation: DesktopSavedEnvironmentSecretProtectionOperation,
142 environmentId: Schema.String,
143 registryPath: Schema.String,
144 cause: Schema.Defect(),
145 },
146) {
147 override get message(): string {
148 return `Desktop saved-environment secret protection failed during ${this.operation} for environment ${this.environmentId} at ${this.registryPath}.`;
149 }
150}
151
144152export type DesktopSavedEnvironmentsReadRegistryError =
145153 | DesktopSavedEnvironmentsReadError
146154 | DesktopSavedEnvironmentsDocumentDecodeError;
......@@ -152,11 +160,11 @@ export type DesktopSavedEnvironmentsMutationError =
152160export type DesktopSavedEnvironmentsGetSecretError =
153161 | DesktopSavedEnvironmentsReadRegistryError
154162 | DesktopSavedEnvironmentSecretDecodeError
155 | ElectronSafeStorage.ElectronSafeStorageError;
163 | DesktopSavedEnvironmentSecretProtectionError;
156164
157165export type DesktopSavedEnvironmentsSetSecretError =
158166 | DesktopSavedEnvironmentsMutationError
159 | ElectronSafeStorage.ElectronSafeStorageError;
167 | DesktopSavedEnvironmentSecretProtectionError;
160168
161169export class DesktopSavedEnvironments extends Context.Service<
162170 DesktopSavedEnvironments,
......@@ -276,19 +284,45 @@ const writeRegistryDocument = Effect.fn("desktop.savedEnvironments.writeRegistry
276284 const directory = input.path.dirname(input.registryPath);
277285 const tempPath = `${input.registryPath}.${process.pid}.${input.suffix}.tmp`;
278286 const encoded = yield* encodeSavedEnvironmentRegistryDocumentJson(input.document).pipe(
279 Effect.mapError((cause) => writeError("encode-registry", input.registryPath, cause)),
287 Effect.mapError(
288 (cause) =>
289 new DesktopSavedEnvironmentsWriteError({
290 operation: "encode-registry",
291 path: input.registryPath,
292 cause,
293 }),
294 ),
295 );
296 yield* input.fileSystem.makeDirectory(directory, { recursive: true }).pipe(
297 Effect.mapError(
298 (cause) =>
299 new DesktopSavedEnvironmentsWriteError({
300 operation: "create-directory",
301 path: directory,
302 cause,
303 }),
304 ),
305 );
306 yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`).pipe(
307 Effect.mapError(
308 (cause) =>
309 new DesktopSavedEnvironmentsWriteError({
310 operation: "write-temporary-file",
311 path: tempPath,
312 cause,
313 }),
314 ),
315 );
316 yield* input.fileSystem.rename(tempPath, input.registryPath).pipe(
317 Effect.mapError(
318 (cause) =>
319 new DesktopSavedEnvironmentsWriteError({
320 operation: "replace-registry-file",
321 path: input.registryPath,
322 cause,
323 }),
324 ),
280325 );
281 yield* input.fileSystem
282 .makeDirectory(directory, { recursive: true })
283 .pipe(Effect.mapError((cause) => writeError("create-directory", directory, cause)));
284 yield* input.fileSystem
285 .writeFileString(tempPath, `${encoded}\n`)
286 .pipe(Effect.mapError((cause) => writeError("write-temporary-file", tempPath, cause)));
287 yield* input.fileSystem
288 .rename(tempPath, input.registryPath)
289 .pipe(
290 Effect.mapError((cause) => writeError("replace-registry-file", input.registryPath, cause)),
291 );
292326 },
293327);
294328
......@@ -341,8 +375,13 @@ export const make = Effect.gen(function* () {
341375 const writeDocument = (document: SavedEnvironmentRegistryDocument) =>
342376 crypto.randomUUIDv4.pipe(
343377 Effect.map((uuid) => uuid.replace(/-/g, "")),
344 Effect.mapError((cause) =>
345 writeError("create-temporary-file-name", environment.savedEnvironmentRegistryPath, cause),
378 Effect.mapError(
379 (cause) =>
380 new DesktopSavedEnvironmentsWriteError({
381 operation: "create-temporary-file-name",
382 path: environment.savedEnvironmentRegistryPath,
383 cause,
384 }),
346385 ),
347386 Effect.flatMap((suffix) =>
348387 writeRegistryDocument({
......@@ -396,7 +435,21 @@ export const make = Effect.gen(function* () {
396435 document.records.find((record) => record.environmentId === environmentId)
397436 ?.encryptedBearerToken,
398437 );
399 if (Option.isNone(encoded) || !(yield* safeStorage.isEncryptionAvailable)) {
438 if (Option.isNone(encoded)) {
439 return Option.none<string>();
440 }
441 const encryptionAvailable = yield* safeStorage.isEncryptionAvailable.pipe(
442 Effect.mapError(
443 (cause) =>
444 new DesktopSavedEnvironmentSecretProtectionError({
445 operation: "check-encryption-availability",
446 environmentId,
447 registryPath: environment.savedEnvironmentRegistryPath,
448 cause,
449 }),
450 ),
451 );
452 if (!encryptionAvailable) {
400453 return Option.none<string>();
401454 }
402455
......@@ -405,7 +458,19 @@ export const make = Effect.gen(function* () {
405458 environment.savedEnvironmentRegistryPath,
406459 encoded.value,
407460 );
408 return Option.some(yield* safeStorage.decryptString(secretBytes));
461 return Option.some(
462 yield* safeStorage.decryptString(secretBytes).pipe(
463 Effect.mapError(
464 (cause) =>
465 new DesktopSavedEnvironmentSecretProtectionError({
466 operation: "decrypt-secret",
467 environmentId,
468 registryPath: environment.savedEnvironmentRegistryPath,
469 cause,
470 }),
471 ),
472 ),
473 );
409474 }),
410475 setSecret: Effect.fn("desktop.savedEnvironments.setSecret")(function* (input) {
411476 const { environmentId, secret } = input;
......@@ -415,11 +480,34 @@ export const make = Effect.gen(function* () {
415480 environment.savedEnvironmentRegistryPath,
416481 );
417482
418 if (!(yield* safeStorage.isEncryptionAvailable)) {
483 const encryptionAvailable = yield* safeStorage.isEncryptionAvailable.pipe(
484 Effect.mapError(
485 (cause) =>
486 new DesktopSavedEnvironmentSecretProtectionError({
487 operation: "check-encryption-availability",
488 environmentId,
489 registryPath: environment.savedEnvironmentRegistryPath,
490 cause,
491 }),
492 ),
493 );
494 if (!encryptionAvailable) {
419495 return false;
420496 }
421497
422 const encryptedBearerToken = Encoding.encodeBase64(yield* safeStorage.encryptString(secret));
498 const encryptedBearerToken = Encoding.encodeBase64(
499 yield* safeStorage.encryptString(secret).pipe(
500 Effect.mapError(
501 (cause) =>
502 new DesktopSavedEnvironmentSecretProtectionError({
503 operation: "encrypt-secret",
504 environmentId,
505 registryPath: environment.savedEnvironmentRegistryPath,
506 cause,
507 }),
508 ),
509 ),
510 );
423511 let found = false;
424512 const nextDocument: SavedEnvironmentRegistryDocument = {
425513 version: document.version,