diff --git a/src/main/runtime/rpc/mobile-socket-wiring.test.ts b/src/main/runtime/rpc/mobile-socket-wiring.test.ts index 5aae8592913..b53f20e572e 100644 --- a/src/main/runtime/rpc/mobile-socket-wiring.test.ts +++ b/src/main/runtime/rpc/mobile-socket-wiring.test.ts @@ -139,12 +139,16 @@ describe('MobileSocketWiring', () => { expect(wiring.connectionCount).toBe(0) }) - it('reports an unknown device token on the direct path and closes 4001', () => { + it('closes an unknown-token socket even when reporting the failure throws', () => { const desktop = generateKeyPair() const phone = generateKeyPair() const ws = new FakeSocket() const transport = new FakeTransport() - const onUnpairedDeviceAuthFailure = vi.fn() + const notificationError = new Error('renderer exited') + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + const onUnpairedDeviceAuthFailure = vi.fn(() => { + throw notificationError + }) const wiring = new MobileSocketWiring({ deviceRegistry: registryFor('device-1', 'valid-token'), e2eeKeypair: { @@ -167,15 +171,23 @@ describe('MobileSocketWiring', () => { }) ) const sharedKey = deriveSharedKey(phone.secretKey, desktop.publicKey) - transport.receive( - ws, - encrypt(JSON.stringify({ type: 'e2ee_auth', deviceToken: 'stale-token' }), sharedKey) - ) + expect(() => + transport.receive( + ws, + encrypt(JSON.stringify({ type: 'e2ee_auth', deviceToken: 'stale-token' }), sharedKey) + ) + ).not.toThrow() expect(onUnpairedDeviceAuthFailure).toHaveBeenCalledOnce() expect(onUnpairedDeviceAuthFailure).toHaveBeenCalledWith({ transport: 'direct' }) + expect(consoleError).toHaveBeenCalledWith( + '[mobile] Failed to report unpaired-device auth failure:', + notificationError + ) expect(transport.setClientId).not.toHaveBeenCalled() expect(ws.close).toHaveBeenCalledWith(4001, 'Unauthorized') + expect(wiring.channelCount).toBe(0) + consoleError.mockRestore() }) it('reports auth encrypted to a stale desktop key on the direct path', () => { diff --git a/src/main/runtime/rpc/mobile-socket-wiring.ts b/src/main/runtime/rpc/mobile-socket-wiring.ts index bcb212a0941..637b5185866 100644 --- a/src/main/runtime/rpc/mobile-socket-wiring.ts +++ b/src/main/runtime/rpc/mobile-socket-wiring.ts @@ -155,12 +155,18 @@ export class MobileSocketWiring { this.onReady?.(socket) }, onError: (code, reason) => { - if (code === 4001 && reason === 'Unauthorized') { - this.onUnpairedDeviceAuthFailure?.(metadata) - } + const reportUnpairedDevice = code === 4001 && reason === 'Unauthorized' this.channels.get(ws)?.destroy() this.channels.delete(ws) ws.close(code, reason) + if (reportUnpairedDevice) { + try { + this.onUnpairedDeviceAuthFailure?.(metadata) + } catch (error) { + // Why: renderer teardown can make UI delivery throw; auth cleanup must remain authoritative. + console.error('[mobile] Failed to report unpaired-device auth failure:', error) + } + } } }) channel.onMessage((plaintext, reply, sendBinary) => {