From a352d3173e56f4eb65e46585a06fb6e22726fb5e Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Wed, 6 May 2026 09:15:45 +0200 Subject: [PATCH 1/4] chore(deps): update electron to v42.0.0 --- package.json | 2 +- pnpm-lock.yaml | 50 ++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 5d9021dc5..d89c9301c 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "concurrently": "9.2.1", "date-fns": "4.1.0", "dotenv": "17.4.2", - "electron": "41.5.0", + "electron": "42.0.0", "electron-builder": "26.8.1", "final-form": "5.0.0", "graphql": "16.13.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f80bd9b2..d7bba8ec2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 6.8.3 menubar: specifier: 9.5.2 - version: 9.5.2(electron@41.5.0) + version: 9.5.2(electron@42.0.0) react: specifier: 19.2.5 version: 19.2.5 @@ -145,8 +145,8 @@ importers: specifier: 17.4.2 version: 17.4.2 electron: - specifier: 41.5.0 - version: 41.5.0 + specifier: 42.0.0 + version: 42.0.0 electron-builder: specifier: 26.8.1 version: 26.8.1(electron-builder-squirrel-windows@26.8.1) @@ -378,6 +378,10 @@ packages: resolution: {integrity: sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==} engines: {node: '>=14'} + '@electron/get@5.0.0': + resolution: {integrity: sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==} + engines: {node: '>=22.12.0'} + '@electron/notarize@2.5.0': resolution: {integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==} engines: {node: '>= 10.0.0'} @@ -2004,9 +2008,9 @@ packages: engines: {node: '>= 12.20.55'} hasBin: true - electron@41.5.0: - resolution: {integrity: sha512-x9j9//PubUA4EjDtQbZhtk3prolandqCKgit0uCIqc1jb8FTskPbnJtxcDFB1aejczJcuERgjPixBUaMwoWyJg==} - engines: {node: '>= 12.20.55'} + electron@42.0.0: + resolution: {integrity: sha512-in5jnW/Ywy3Rh3FPr4MR80exPEkywKYAmDJRZ/gIKlr8VXEi3zXgiAZbf0Si7KRccHTF2y8euVMRz7M6HqTjMA==} + engines: {node: '>= 22.12.0'} hasBin: true emoji-regex@10.6.0: @@ -2030,6 +2034,10 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -3593,6 +3601,10 @@ packages: resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==} engines: {node: '>=18.17'} + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -4150,6 +4162,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@electron/get@5.0.0': + dependencies: + debug: 4.4.3 + env-paths: 3.0.0 + graceful-fs: 4.2.11 + progress: 2.0.3 + semver: 7.7.4 + sumchecker: 3.0.1 + optionalDependencies: + undici: 7.25.0 + transitivePeerDependencies: + - supports-color + '@electron/notarize@2.5.0': dependencies: debug: 4.4.3 @@ -5975,9 +6000,9 @@ snapshots: transitivePeerDependencies: - supports-color - electron@41.5.0: + electron@42.0.0: dependencies: - '@electron/get': 2.0.3 + '@electron/get': 5.0.0 '@types/node': 24.12.2 extract-zip: 2.0.1 transitivePeerDependencies: @@ -6000,6 +6025,8 @@ snapshots: env-paths@2.2.1: {} + env-paths@3.0.0: {} + environment@1.1.0: {} err-code@2.0.3: {} @@ -6757,9 +6784,9 @@ snapshots: math-intrinsics@1.1.0: {} - menubar@9.5.2(electron@41.5.0): + menubar@9.5.2(electron@42.0.0): dependencies: - electron: 41.5.0 + electron: 42.0.0 electron-positioner: 4.1.0 merge-stream@2.0.0: {} @@ -7474,6 +7501,9 @@ snapshots: undici@6.25.0: {} + undici@7.25.0: + optional: true + unicorn-magic@0.3.0: {} unique-string@1.0.0: From b814c522a833f702e67a1d5aba39f23b7ed233e6 Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Wed, 6 May 2026 09:15:49 +0200 Subject: [PATCH 2/4] refactor(storage): use async safeStorage methods from Electron 42 --- src/main/handlers/storage.test.ts | 4 ++-- src/main/handlers/storage.ts | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/handlers/storage.test.ts b/src/main/handlers/storage.test.ts index 88249b822..67f63cf68 100644 --- a/src/main/handlers/storage.test.ts +++ b/src/main/handlers/storage.test.ts @@ -9,8 +9,8 @@ vi.mock('electron', () => ({ handle: (...args: unknown[]) => handleMock(...args), }, safeStorage: { - encryptString: vi.fn((str: string) => Buffer.from(str)), - decryptString: vi.fn((buf: Buffer) => buf.toString()), + encryptStringAsync: vi.fn(async (str: string) => Buffer.from(str)), + decryptStringAsync: vi.fn(async (buf: Buffer) => buf.toString()), }, })); diff --git a/src/main/handlers/storage.ts b/src/main/handlers/storage.ts index d60ddf02c..5d3493d81 100644 --- a/src/main/handlers/storage.ts +++ b/src/main/handlers/storage.ts @@ -12,16 +12,17 @@ export function registerStorageHandlers(): void { /** * Encrypt a string using Electron's safeStorage and return the encrypted value as a base64 string. */ - handleMainEvent(EVENTS.SAFE_STORAGE_ENCRYPT, (_, value: string) => { - return safeStorage.encryptString(value).toString('base64'); + handleMainEvent(EVENTS.SAFE_STORAGE_ENCRYPT, async (_, value: string) => { + const encrypted = await safeStorage.encryptStringAsync(value); + return encrypted.toString('base64'); }); /** * Decrypt a base64-encoded string using Electron's safeStorage and return the decrypted value. */ - handleMainEvent(EVENTS.SAFE_STORAGE_DECRYPT, (_, value: string) => { + handleMainEvent(EVENTS.SAFE_STORAGE_DECRYPT, async (_, value: string) => { try { - return safeStorage.decryptString(Buffer.from(value, 'base64')); + return await safeStorage.decryptStringAsync(Buffer.from(value, 'base64')); } catch (err) { logError( 'main:safe-storage-decrypt', From 3d31d7d327236e8ea510fd739ce8867dfa8bbab1 Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Wed, 6 May 2026 15:05:48 +0200 Subject: [PATCH 3/4] fix(storage): unwrap result from decryptStringAsync return value --- src/main/handlers/storage.test.ts | 41 ++++++++++++++++++++++++++++++- src/main/handlers/storage.ts | 5 +++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/main/handlers/storage.test.ts b/src/main/handlers/storage.test.ts index 67f63cf68..b9f323dbb 100644 --- a/src/main/handlers/storage.test.ts +++ b/src/main/handlers/storage.test.ts @@ -2,6 +2,11 @@ import { EVENTS } from '../../shared/events'; import { registerStorageHandlers } from './storage'; +type IpcHandler = ( + event: unknown, + ...args: unknown[] +) => unknown | Promise; + const handleMock = vi.fn(); vi.mock('electron', () => ({ @@ -10,7 +15,10 @@ vi.mock('electron', () => ({ }, safeStorage: { encryptStringAsync: vi.fn(async (str: string) => Buffer.from(str)), - decryptStringAsync: vi.fn(async (buf: Buffer) => buf.toString()), + decryptStringAsync: vi.fn(async (buf: Buffer) => ({ + shouldReEncrypt: false, + result: buf.toString(), + })), }, })); @@ -23,6 +31,16 @@ vi.mock('../../shared/logger', async (importOriginal) => { }; }); +function getHandler(event: string): IpcHandler { + const call = handleMock.mock.calls.find( + (entry: unknown[]) => entry[0] === event, + ); + if (!call) { + throw new Error(`No handler registered for ${event}`); + } + return call[1] as IpcHandler; +} + describe('main/handlers/storage.ts', () => { describe('registerStorageHandlers', () => { it('registers handlers without throwing', () => { @@ -39,5 +57,26 @@ describe('main/handlers/storage.ts', () => { expect(registeredHandlers).toContain(EVENTS.SAFE_STORAGE_ENCRYPT); expect(registeredHandlers).toContain(EVENTS.SAFE_STORAGE_DECRYPT); }); + + it('encrypt handler returns a base64 string', async () => { + registerStorageHandlers(); + const encrypt = getHandler(EVENTS.SAFE_STORAGE_ENCRYPT); + + const result = await encrypt({}, 'plain-token'); + + expect(typeof result).toBe('string'); + expect(result).toBe(Buffer.from('plain-token').toString('base64')); + }); + + it('decrypt handler returns the unwrapped plaintext string', async () => { + registerStorageHandlers(); + const decrypt = getHandler(EVENTS.SAFE_STORAGE_DECRYPT); + + const ciphertext = Buffer.from('plain-token').toString('base64'); + const result = await decrypt({}, ciphertext); + + expect(typeof result).toBe('string'); + expect(result).toBe('plain-token'); + }); }); }); diff --git a/src/main/handlers/storage.ts b/src/main/handlers/storage.ts index 5d3493d81..14a1ae708 100644 --- a/src/main/handlers/storage.ts +++ b/src/main/handlers/storage.ts @@ -22,7 +22,10 @@ export function registerStorageHandlers(): void { */ handleMainEvent(EVENTS.SAFE_STORAGE_DECRYPT, async (_, value: string) => { try { - return await safeStorage.decryptStringAsync(Buffer.from(value, 'base64')); + const { result } = await safeStorage.decryptStringAsync( + Buffer.from(value, 'base64'), + ); + return result; } catch (err) { logError( 'main:safe-storage-decrypt', From e1049f51cf338ee3d6be0e020d919cf214153abe Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Wed, 6 May 2026 16:30:22 +0200 Subject: [PATCH 4/4] test(storage): pin mock shape to Electron's SafeStorage type --- src/main/handlers/storage.test.ts | 44 ++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/main/handlers/storage.test.ts b/src/main/handlers/storage.test.ts index b9f323dbb..9bb1ac4aa 100644 --- a/src/main/handlers/storage.test.ts +++ b/src/main/handlers/storage.test.ts @@ -1,3 +1,6 @@ +import type { SafeStorage } from 'electron'; +import { safeStorage } from 'electron'; + import { EVENTS } from '../../shared/events'; import { registerStorageHandlers } from './storage'; @@ -19,7 +22,7 @@ vi.mock('electron', () => ({ shouldReEncrypt: false, result: buf.toString(), })), - }, + } satisfies Pick, })); const logErrorMock = vi.fn(); @@ -78,5 +81,44 @@ describe('main/handlers/storage.ts', () => { expect(typeof result).toBe('string'); expect(result).toBe('plain-token'); }); + + it('decrypt handler unwraps result when shouldReEncrypt is true', async () => { + vi.mocked(safeStorage.decryptStringAsync).mockResolvedValueOnce({ + shouldReEncrypt: true, + result: 'rotated-token', + }); + registerStorageHandlers(); + const decrypt = getHandler(EVENTS.SAFE_STORAGE_DECRYPT); + + const result = await decrypt({}, 'irrelevant'); + + expect(typeof result).toBe('string'); + expect(result).toBe('rotated-token'); + }); + + it('encrypt → decrypt round-trip preserves the original string', async () => { + registerStorageHandlers(); + const encrypt = getHandler(EVENTS.SAFE_STORAGE_ENCRYPT); + const decrypt = getHandler(EVENTS.SAFE_STORAGE_DECRYPT); + + const ciphertext = (await encrypt({}, 'round-trip-token')) as string; + const plaintext = await decrypt({}, ciphertext); + + expect(plaintext).toBe('round-trip-token'); + }); + + it('decrypt handler rethrows and logs on safeStorage failure', async () => { + const failure = new Error('keychain unavailable'); + vi.mocked(safeStorage.decryptStringAsync).mockRejectedValueOnce(failure); + registerStorageHandlers(); + const decrypt = getHandler(EVENTS.SAFE_STORAGE_DECRYPT); + + await expect(decrypt({}, 'irrelevant')).rejects.toBe(failure); + expect(logErrorMock).toHaveBeenCalledWith( + 'main:safe-storage-decrypt', + expect.any(String), + failure, + ); + }); }); });