Skip to content

Conversation

@Tranquil-Flow
Copy link
Contributor

@Tranquil-Flow Tranquil-Flow commented Jan 19, 2026

Description

Previously internet issues could cause websocket disconnection, which would lead to the selfApp state being cleared which could cause the app to hang in 'Waiting for app...'. Ensured disconnect does not clear the state.

Tested

Used local mobile build on my Android device. After selecting my document during a verification proof flow, I turned on airplane mode, then turned back on. Before the fix, it would be stalled in 'Waiting for app...'. After, it allowed me to click the button to hold to verify and continue.

How to QA

Build mobile app with fix and follow steps I did in the test.


Note

Improves resilience of the proof flow to temporary network loss and reduces stalls.

  • TEE WebSocket reconnection: adds wsReconnectAttempts tracking, _reconnectTeeWebSocket with backoff/timeout, and auto-retries on ready_to_prove; startProving now ensures an open WS (reconnects if needed) before sending payload; resets attempts on success
  • Socket.IO state preservation: selfAppStore no longer clears selfApp on transient errors/disconnects; only clears on intentional client/server disconnects
  • UI polish: ProveScreen auto-enables verify state when content is short after session change to avoid unnecessary scrolling

Written by Cursor Bugbot for commit 39678e3. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Improved automatic TEE websocket reconnection with retry/backoff, reconnect-attempt tracking, and a programmatic reconnect capability in the SDK.
    • Verification screen now auto-scrolls short content after a session change.
  • Bug Fixes

    • Preserves cached app state on transient socket errors/disconnects (only connection identifiers cleared).
    • Proofing flow now ensures an active socket before sending to reduce mid-process failures.
  • Style

    • Standardized, clearer error and warning messages for connection and processing issues.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

📝 Walkthrough

Walkthrough

Preserve cached selfApp on transient socket errors; limit state clearing to socket/sessionId for specific disconnect reasons. Add automatic TEE WebSocket reconnection with tracked wsReconnectAttempts, exponential backoff, and a public _reconnectTeeWebSocket(selfClient) used before proving/payload sends. Auto-mark short verification content as scrolled-to-bottom after session switches.

Changes

Cohort / File(s) Summary
SelfApp store: disconnect & error handling
packages/mobile-sdk-alpha/src/stores/selfAppStore.tsx
Refined connect_error and disconnect handlers to preserve cached selfApp, clear only socket/sessionId for specific reasons, ensure disconnect events match current socket before mutating state, and standardize error/warning logs.
Proving: TEE WebSocket reconnection & validation
packages/mobile-sdk-alpha/src/proving/provingMachine.ts
Added wsReconnectAttempts: number and exported _reconnectTeeWebSocket(selfClient) => Promise<boolean> on ProvingState; implement exponential-backoff reconnects, reset attempts on successful open, attempt reconnect before startProving and payload sends, guard payload sending with active WS refs, and add logging/proof-event traces. Public API surface updated.
UI: verification session auto-scroll re-eval
app/src/screens/verification/ProveScreen.tsx
After sessionId changes, asynchronously re-measure content (via setTimeout(0)) and auto-mark scrolled-to-bottom + initialized for short content (content height <= view height + 50) to avoid React state-race conditions.

Sequence Diagram(s)

sequenceDiagram
  participant UI as "Prover / UI"
  participant ProvingState as "ProvingState"
  participant SelfClient as "SelfClient"
  participant TEE_WS as "TEE WebSocket (TEE)"

  UI->>ProvingState: startProving()
  ProvingState->>ProvingState: check get().wsConnection (activeWsConnection)
  alt ws is OPEN
    ProvingState->>TEE_WS: send payload
    TEE_WS-->>ProvingState: response / ack
    ProvingState-->>UI: proceed with proof
  else ws not OPEN
    ProvingState->>ProvingState: call _reconnectTeeWebSocket(SelfClient)
    ProvingState->>SelfClient: build circuit params & WS URL
    ProvingState->>TEE_WS: attempt open (exponential backoff)
    alt open success
      ProvingState->>ProvingState: reset wsReconnectAttempts
      ProvingState->>TEE_WS: send payload
      TEE_WS-->>ProvingState: response / ack
      ProvingState-->>UI: proceed with proof
    else reconnect failed (exhausted)
      ProvingState-->>UI: emit PROVE_ERROR / abort
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • shazarre

Poem

🔧 Sockets pause, then try again,
Counters tick to count the when.
Cache held safe through fleeting loss,
Short screens scroll with minor gloss.
Reconnect, retry — carry on, then send.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: resolving connection loss issues during proof verification by preventing state clearing on disconnects.
Description check ✅ Passed The PR description covers all required template sections with clear explanations of the changes, testing methodology, and QA steps.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/mobile-sdk-alpha/src/proving/provingMachine.ts`:
- Around line 913-940: The Promise in the reconnection logic can resolve both on
timeout and later on a late "open" event; fix by introducing a local resolved
flag (e.g., let settled = false) or a timeout id and ensure only a single
resolution path runs: in the timeout handler, if not settled set settled=true,
remove the ws event listeners (ws.removeEventListener for
wsHandlers.message/open/error/close), optionally close the ws, and then
resolve(false); in the wsHandlers.open handler, first check if settled is true
(or clear the timeout and set settled=true) before calling set({
wsReconnectAttempts: 0 }) and resolve(true); ensure cleanup of listeners in the
open path as well to prevent late events from firing. Use the existing symbols
ws, wsHandlers, RECONNECT_TIMEOUT_MS, selfClient, set, and get to implement
this.
♻️ Duplicate comments (1)
packages/mobile-sdk-alpha/src/stores/selfAppStore.tsx (1)

112-119: Stale selfApp can persist across different sessions.

When an intentional disconnect (io server disconnect) occurs, socket and sessionId are cleared but selfApp is preserved. If startAppListener is later called with a different session ID, the condition on line 68 (currentSocket && get().sessionId !== sessionId) evaluates to false because currentSocket is null. The stale selfApp from the previous session remains until the new self_app event arrives.

Consider clearing selfApp on intentional disconnects, or adding an additional guard in startAppListener to clear selfApp when the stored selfApp.sessionId doesn't match the new sessionId.

Comment on lines +913 to +940
return new Promise(resolve => {
const ws = new WebSocket(wsRpcUrl);
const RECONNECT_TIMEOUT_MS = 15000;

const wsHandlers: WsHandlers = {
message: (event: MessageEvent) => get()._handleWebSocketMessage(event, selfClient),
open: () => {
selfClient.logProofEvent('info', 'TEE WebSocket reconnected', context);
set({ wsReconnectAttempts: 0 });
resolve(true);
},
error: (error: Event) => get()._handleWsError(error, selfClient),
close: (event: CloseEvent) => get()._handleWsClose(event, selfClient),
};

set({ wsConnection: ws, wsHandlers });
ws.addEventListener('message', wsHandlers.message);
ws.addEventListener('open', wsHandlers.open);
ws.addEventListener('error', wsHandlers.error);
ws.addEventListener('close', wsHandlers.close);

setTimeout(() => {
if (ws.readyState !== WebSocket.OPEN) {
selfClient.logProofEvent('warn', 'TEE WebSocket reconnection timeout', context);
resolve(false);
}
}, RECONNECT_TIMEOUT_MS);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Race condition between timeout and successful connection.

If the WebSocket opens after the 15-second timeout fires, both code paths execute:

  1. Timeout fires → resolve(false)
  2. WebSocket opens → set({ wsReconnectAttempts: 0 }) runs, then resolve(true) (no-op since already resolved)

The wsReconnectAttempts counter gets incorrectly reset to 0 even though the reconnection was considered failed. Additionally, the event listeners remain attached and could fire unexpectedly on the "late" connection.

🔧 Suggested fix: track resolution state and clean up on timeout
     return new Promise(resolve => {
       const ws = new WebSocket(wsRpcUrl);
       const RECONNECT_TIMEOUT_MS = 15000;
+      let resolved = false;

       const wsHandlers: WsHandlers = {
         message: (event: MessageEvent) => get()._handleWebSocketMessage(event, selfClient),
         open: () => {
+          if (resolved) return;
+          resolved = true;
           selfClient.logProofEvent('info', 'TEE WebSocket reconnected', context);
           set({ wsReconnectAttempts: 0 });
           resolve(true);
         },
         error: (error: Event) => get()._handleWsError(error, selfClient),
         close: (event: CloseEvent) => get()._handleWsClose(event, selfClient),
       };

       set({ wsConnection: ws, wsHandlers });
       ws.addEventListener('message', wsHandlers.message);
       ws.addEventListener('open', wsHandlers.open);
       ws.addEventListener('error', wsHandlers.error);
       ws.addEventListener('close', wsHandlers.close);

       setTimeout(() => {
-        if (ws.readyState !== WebSocket.OPEN) {
+        if (!resolved && ws.readyState !== WebSocket.OPEN) {
+          resolved = true;
           selfClient.logProofEvent('warn', 'TEE WebSocket reconnection timeout', context);
+          // Clean up the failed connection
+          ws.removeEventListener('message', wsHandlers.message);
+          ws.removeEventListener('open', wsHandlers.open);
+          ws.removeEventListener('error', wsHandlers.error);
+          ws.removeEventListener('close', wsHandlers.close);
+          ws.close();
+          set({ wsConnection: null, wsHandlers: null });
           resolve(false);
         }
       }, RECONNECT_TIMEOUT_MS);
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return new Promise(resolve => {
const ws = new WebSocket(wsRpcUrl);
const RECONNECT_TIMEOUT_MS = 15000;
const wsHandlers: WsHandlers = {
message: (event: MessageEvent) => get()._handleWebSocketMessage(event, selfClient),
open: () => {
selfClient.logProofEvent('info', 'TEE WebSocket reconnected', context);
set({ wsReconnectAttempts: 0 });
resolve(true);
},
error: (error: Event) => get()._handleWsError(error, selfClient),
close: (event: CloseEvent) => get()._handleWsClose(event, selfClient),
};
set({ wsConnection: ws, wsHandlers });
ws.addEventListener('message', wsHandlers.message);
ws.addEventListener('open', wsHandlers.open);
ws.addEventListener('error', wsHandlers.error);
ws.addEventListener('close', wsHandlers.close);
setTimeout(() => {
if (ws.readyState !== WebSocket.OPEN) {
selfClient.logProofEvent('warn', 'TEE WebSocket reconnection timeout', context);
resolve(false);
}
}, RECONNECT_TIMEOUT_MS);
});
return new Promise(resolve => {
const ws = new WebSocket(wsRpcUrl);
const RECONNECT_TIMEOUT_MS = 15000;
let resolved = false;
const wsHandlers: WsHandlers = {
message: (event: MessageEvent) => get()._handleWebSocketMessage(event, selfClient),
open: () => {
if (resolved) return;
resolved = true;
selfClient.logProofEvent('info', 'TEE WebSocket reconnected', context);
set({ wsReconnectAttempts: 0 });
resolve(true);
},
error: (error: Event) => get()._handleWsError(error, selfClient),
close: (event: CloseEvent) => get()._handleWsClose(event, selfClient),
};
set({ wsConnection: ws, wsHandlers });
ws.addEventListener('message', wsHandlers.message);
ws.addEventListener('open', wsHandlers.open);
ws.addEventListener('error', wsHandlers.error);
ws.addEventListener('close', wsHandlers.close);
setTimeout(() => {
if (!resolved && ws.readyState !== WebSocket.OPEN) {
resolved = true;
selfClient.logProofEvent('warn', 'TEE WebSocket reconnection timeout', context);
// Clean up the failed connection
ws.removeEventListener('message', wsHandlers.message);
ws.removeEventListener('open', wsHandlers.open);
ws.removeEventListener('error', wsHandlers.error);
ws.removeEventListener('close', wsHandlers.close);
ws.close();
set({ wsConnection: null, wsHandlers: null });
resolve(false);
}
}, RECONNECT_TIMEOUT_MS);
});
🤖 Prompt for AI Agents
In `@packages/mobile-sdk-alpha/src/proving/provingMachine.ts` around lines 913 -
940, The Promise in the reconnection logic can resolve both on timeout and later
on a late "open" event; fix by introducing a local resolved flag (e.g., let
settled = false) or a timeout id and ensure only a single resolution path runs:
in the timeout handler, if not settled set settled=true, remove the ws event
listeners (ws.removeEventListener for wsHandlers.message/open/error/close),
optionally close the ws, and then resolve(false); in the wsHandlers.open
handler, first check if settled is true (or clear the timeout and set
settled=true) before calling set({ wsReconnectAttempts: 0 }) and resolve(true);
ensure cleanup of listeners in the open path as well to prevent late events from
firing. Use the existing symbols ws, wsHandlers, RECONNECT_TIMEOUT_MS,
selfClient, set, and get to implement this.

selfClient.logProofEvent('info', 'TEE WebSocket reconnected', context);
set({ wsReconnectAttempts: 0 });
resolve(true);
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WebSocket reconnection skips required hello handshake protocol

High Severity

The _reconnectTeeWebSocket function's open handler just logs and resolves true without calling _handleWsOpen. The original connection flow in initTeeConnection uses open: () => get()._handleWsOpen(selfClient) which sends the required openpassport_hello message with the client's public key, receives the server's attestation, and derives the shared encryption key. Without this handshake, the reconnected WebSocket has no valid session with the server, and subsequent proof requests will fail because the server doesn't recognize the connection or the preserved UUID.

Fix in Cursor Fix in Web

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/screens/verification/ProveScreen.tsx (1)

171-187: Prevent stale timeout from marking scroll complete for the wrong session

The new setTimeout(0) can run after a session switch/unmount and set hasScrolledToBottom using stale measurements, potentially enabling verification without scrolling. Add cleanup to cancel pending timeouts.

✅ Suggested fix (cleanup pending timeout)
  useEffect(() => {
    if (!isFocused || !selectedApp) {
      return;
    }

+   let resetTimeout: ReturnType<typeof setTimeout> | undefined;
+
    // Reset scroll state tracking for new session
    if (selectedAppRef.current?.sessionId !== selectedApp.sessionId) {
      hasInitializedScrollStateRef.current = false;
      setHasScrolledToBottom(false);

      // After state reset, check if content is short using current measurements.
      // Use setTimeout(0) to ensure we read values AFTER React processes the reset,
      // without adding measurements to dependencies (which causes race conditions).
-     setTimeout(() => {
+     resetTimeout = setTimeout(() => {
        const hasMeasurements = scrollViewContentHeight > 0 && scrollViewHeight > 0;
        const isShort = scrollViewContentHeight <= scrollViewHeight + 50;

        if (hasMeasurements && isShort) {
          setHasScrolledToBottom(true);
          hasInitializedScrollStateRef.current = true;
        }
      }, 0);
    }

    setDefaultDocumentTypeIfNeeded();
    ...
-  }, [selectedApp?.sessionId, isFocused, selfClient]);
+    return () => {
+      if (resetTimeout) {
+        clearTimeout(resetTimeout);
+      }
+    };
+  }, [selectedApp?.sessionId, isFocused, selfClient]);
🤖 Fix all issues with AI agents
In `@packages/mobile-sdk-alpha/src/proving/provingMachine.ts`:
- Around line 845-863: The reconnection logic allows overlapping attempts
between _handleWsClose's backoff schedule and startProving, so add a single
in‑flight reconnect guard (e.g., a private field like reconnectPromise or
isReconnecting) and use it inside _reconnectTeeWebSocket, _handleWsClose and
startProving to ensure only one reconnect runs: when scheduling a backoff in the
ready_to_prove branch set the guard before calling setTimeout, have
_reconnectTeeWebSocket return/reuse the same Promise if the guard is set, have
startProving check/await the guard instead of spawning a new connection, and
clear the guard on success/final failure while still incrementing
wsReconnectAttempts and updating wsConnection as before.

setHasScrolledToBottom(true);
hasInitializedScrollStateRef.current = true;
}
}, 0);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale closure uses old measurements for new session

Medium Severity

The setTimeout(0) callback captures scrollViewContentHeight and scrollViewHeight from the closure when the effect runs, not when the callback executes. When switching sessions, if the previous session had short content and the new session has long content, the timeout uses the stale (short) measurements to set hasScrolledToBottom=true and hasInitializedScrollStateRef.current=true. This prevents the other useEffect (which checks hasInitializedScrollStateRef.current) from properly re-evaluating with new measurements, potentially allowing users to skip scrolling through required disclosures.

Fix in Cursor Fix in Web

selfClient.logProofEvent('info', 'TEE WebSocket reconnected', context);
set({ wsReconnectAttempts: 0 });
resolve(true);
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reconnect counter reset bypasses max attempts limit

Medium Severity

The open handler in _reconnectTeeWebSocket resets wsReconnectAttempts to 0 whenever the WebSocket opens, while _handleWsClose increments the counter to enforce MAX_RECONNECT_ATTEMPTS. If a connection opens but immediately closes (which will happen due to the missing hello handshake), the counter resets to 0 before _handleWsClose runs again. This creates an infinite loop of reconnection attempts, effectively bypassing the 3-attempt limit meant to prevent endless retries.

Additional Locations (1)

Fix in Cursor Fix in Web

@Tranquil-Flow Tranquil-Flow force-pushed the hotfix/socket-connection-issue branch from 7e35e08 to 39678e3 Compare January 27, 2026 06:27
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/mobile-sdk-alpha/src/proving/provingMachine.ts`:
- Around line 884-941: The reconnect logic in _reconnectTeeWebSocket currently
resolves on WebSocket 'open' without running the full TEE handshake; update the
wsHandlers.open to call get()._handleWsOpen(...) (passing the open event and
selfClient) and only resolve true when the handshake completes (e.g., when the
prover state or a CONNECT_SUCCESS event/flag is observed), otherwise wait until
the RECONNECT_TIMEOUT_MS then resolve false; ensure wsHandlers and set({
wsConnection, wsHandlers }) remain consistent and preserve existing error/close
handling so startProving uses the fresh sharedKey/uuid after a successful
_handleWsOpen-driven handshake.
🧹 Nitpick comments (1)
packages/mobile-sdk-alpha/src/proving/provingMachine.ts (1)

845-877: Potential concurrent reconnection attempts remain possible.

The backoff reconnection in _handleWsClose schedules via setTimeout, while startProving can trigger an immediate reconnection. If startProving is invoked during the backoff delay, both paths may spawn concurrent WebSocket connections, racing the store state.

Consider adding an in-flight promise guard to ensure only one reconnection attempt runs at a time:

🔧 Suggested approach
+let wsReconnectInFlight: Promise<boolean> | null = null;

 // In _handleWsClose backoff path:
 setTimeout(() => {
   if (get().currentState === 'ready_to_prove') {
-    get()._reconnectTeeWebSocket(selfClient);
+    if (!wsReconnectInFlight) {
+      wsReconnectInFlight = get()._reconnectTeeWebSocket(selfClient).finally(() => {
+        wsReconnectInFlight = null;
+      });
+    }
   }
 }, backoffMs);

 // In startProving reconnection path:
-const reconnected = await get()._reconnectTeeWebSocket(selfClient);
+if (!wsReconnectInFlight) {
+  wsReconnectInFlight = get()._reconnectTeeWebSocket(selfClient).finally(() => {
+    wsReconnectInFlight = null;
+  });
+}
+const reconnected = await wsReconnectInFlight;

Comment on lines +884 to +941
/**
* Re-establishes the TEE WebSocket connection using stored circuit parameters.
* Called automatically when connection is lost in ready_to_prove state.
*/
_reconnectTeeWebSocket: async (selfClient: SelfClient): Promise<boolean> => {
const context = createProofContext(selfClient, '_reconnectTeeWebSocket');
const { passportData, circuitType } = get();

if (!passportData || !circuitType) {
selfClient.logProofEvent('error', 'Reconnect failed: missing prerequisites', context);
return false;
}

const typedCircuitType = circuitType as 'disclose' | 'register' | 'dsc';
const circuitName =
typedCircuitType === 'disclose'
? passportData.documentCategory === 'aadhaar'
? 'disclose_aadhaar'
: 'disclose'
: getCircuitNameFromPassportData(passportData, typedCircuitType as 'register' | 'dsc');

const wsRpcUrl = resolveWebSocketUrl(selfClient, typedCircuitType, passportData as PassportData, circuitName);
if (!wsRpcUrl) {
selfClient.logProofEvent('error', 'Reconnect failed: no WebSocket URL', context);
return false;
}

selfClient.logProofEvent('info', 'TEE WebSocket reconnection started', context);

return new Promise(resolve => {
const ws = new WebSocket(wsRpcUrl);
const RECONNECT_TIMEOUT_MS = 15000;

const wsHandlers: WsHandlers = {
message: (event: MessageEvent) => get()._handleWebSocketMessage(event, selfClient),
open: () => {
selfClient.logProofEvent('info', 'TEE WebSocket reconnected', context);
set({ wsReconnectAttempts: 0 });
resolve(true);
},
error: (error: Event) => get()._handleWsError(error, selfClient),
close: (event: CloseEvent) => get()._handleWsClose(event, selfClient),
};

set({ wsConnection: ws, wsHandlers });
ws.addEventListener('message', wsHandlers.message);
ws.addEventListener('open', wsHandlers.open);
ws.addEventListener('error', wsHandlers.error);
ws.addEventListener('close', wsHandlers.close);

setTimeout(() => {
if (ws.readyState !== WebSocket.OPEN) {
selfClient.logProofEvent('warn', 'TEE WebSocket reconnection timeout', context);
resolve(false);
}
}, RECONNECT_TIMEOUT_MS);
});
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Reconnection does not re-establish TEE session (hello + attestation).

The open handler in _reconnectTeeWebSocket only resets the counter and resolves—it doesn't invoke _handleWsOpen, which is responsible for sending the hello message and initiating the attestation/key-derivation flow.

After reconnection:

  1. WebSocket opens → resolve(true)
  2. startProving proceeds using the old sharedKey and uuid
  3. Server hasn't received hello, so it doesn't recognize this client
  4. Payload encryption/decryption will fail or be rejected

The reconnected WebSocket must complete the full handshake before the connection is considered ready.

🔧 Suggested fix: Invoke _handleWsOpen and wait for CONNECT_SUCCESS
 _reconnectTeeWebSocket: async (selfClient: SelfClient): Promise<boolean> => {
   const context = createProofContext(selfClient, '_reconnectTeeWebSocket');
   const { passportData, circuitType } = get();

   if (!passportData || !circuitType) {
     selfClient.logProofEvent('error', 'Reconnect failed: missing prerequisites', context);
     return false;
   }

   // ... URL resolution code ...

   selfClient.logProofEvent('info', 'TEE WebSocket reconnection started', context);

   return new Promise(resolve => {
     const ws = new WebSocket(wsRpcUrl);
     const RECONNECT_TIMEOUT_MS = 15000;
+    let resolved = false;

     const wsHandlers: WsHandlers = {
       message: (event: MessageEvent) => get()._handleWebSocketMessage(event, selfClient),
-      open: () => {
-        selfClient.logProofEvent('info', 'TEE WebSocket reconnected', context);
-        set({ wsReconnectAttempts: 0 });
-        resolve(true);
-      },
+      open: () => get()._handleWsOpen(selfClient),
       error: (error: Event) => get()._handleWsError(error, selfClient),
       close: (event: CloseEvent) => get()._handleWsClose(event, selfClient),
     };

     set({ wsConnection: ws, wsHandlers });
     ws.addEventListener('message', wsHandlers.message);
     ws.addEventListener('open', wsHandlers.open);
     ws.addEventListener('error', wsHandlers.error);
     ws.addEventListener('close', wsHandlers.close);

+    // Subscribe to actor state to detect successful reconnection
+    if (actor) {
+      const unsubscribe = actor.subscribe(state => {
+        if (resolved) return;
+        if (state.matches('ready_to_prove')) {
+          resolved = true;
+          selfClient.logProofEvent('info', 'TEE WebSocket reconnected', context);
+          set({ wsReconnectAttempts: 0 });
+          unsubscribe.unsubscribe();
+          resolve(true);
+        } else if (state.matches('error')) {
+          resolved = true;
+          unsubscribe.unsubscribe();
+          resolve(false);
+        }
+      });
+    }

     setTimeout(() => {
-      if (ws.readyState !== WebSocket.OPEN) {
+      if (!resolved) {
+        resolved = true;
         selfClient.logProofEvent('warn', 'TEE WebSocket reconnection timeout', context);
+        // Cleanup failed connection
+        ws.removeEventListener('message', wsHandlers.message);
+        ws.removeEventListener('open', wsHandlers.open);
+        ws.removeEventListener('error', wsHandlers.error);
+        ws.removeEventListener('close', wsHandlers.close);
+        ws.close();
+        set({ wsConnection: null, wsHandlers: null });
         resolve(false);
       }
     }, RECONNECT_TIMEOUT_MS);
   });
 },
🤖 Prompt for AI Agents
In `@packages/mobile-sdk-alpha/src/proving/provingMachine.ts` around lines 884 -
941, The reconnect logic in _reconnectTeeWebSocket currently resolves on
WebSocket 'open' without running the full TEE handshake; update the
wsHandlers.open to call get()._handleWsOpen(...) (passing the open event and
selfClient) and only resolve true when the handshake completes (e.g., when the
prover state or a CONNECT_SUCCESS event/flag is observed), otherwise wait until
the RECONNECT_TIMEOUT_MS then resolve false; ensure wsHandlers and set({
wsConnection, wsHandlers }) remain consistent and preserve existing error/close
handling so startProving uses the fresh sharedKey/uuid after a successful
_handleWsOpen-driven handshake.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

selfClient.logProofEvent('warn', 'TEE WebSocket reconnection timeout', context);
resolve(false);
}
}, RECONNECT_TIMEOUT_MS);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reconnection timeout leaks WebSocket without cleanup

Medium Severity

When the 15-second reconnection timeout fires and the WebSocket is not yet open, the code calls resolve(false) but does not close the WebSocket or remove its event handlers. Since set({ wsConnection: ws, wsHandlers }) was already called at line 928, the WebSocket remains in state. If the WebSocket later connects (slow network), the open handler fires and sets wsReconnectAttempts to 0, causing inconsistent state even though the reconnection was deemed failed.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants