{"record":{"id":"32c1f60dd95229ca","repo":"tinyhumansai/openhuman","slug":"transport-manager-tunnel-profile-missing-session","errorCode":null,"errorMessage":"[transport:manager] tunnel profile missing sessionToken or pairingToken","messagePattern":"\\[transport:manager\\] tunnel profile missing sessionToken or pairingToken","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"app/src/services/transport/TransportManager.ts","lineNumber":119,"sourceCode":"    throw new Error(`[transport:manager] unknown profile kind: ${kind}`);\n  }\n\n  /**\n   * Race LAN (with 2 s timeout) against Tunnel.\n   * Whichever responds to `openhuman.ping` first wins.\n   * If LAN wins but later fails, caller should call reset() to re-race.\n   */\n  private async raceLanAndTunnel(): Promise<CoreTransport> {\n    const { rpcUrl, channelId, corePubkey, sessionToken, pairingToken, devicePrivkey } =\n      this.profile;\n\n    if (!channelId || !corePubkey) {\n      throw new Error('[transport:manager] tunnel profile missing channelId or corePubkey');\n    }\n\n    const tunnelToken = sessionToken ?? pairingToken;\n    if (!tunnelToken) {\n      throw new Error('[transport:manager] tunnel profile missing sessionToken or pairingToken');\n    }\n\n    const tunnelTransport = new TunnelTransport(\n      this.backendSocketUrl,\n      channelId,\n      corePubkey,\n      tunnelToken,\n      devicePrivkey,\n      sessionToken ? 'session' : 'pairing'\n    );\n\n    if (!rpcUrl) {\n      // No LAN URL — tunnel only.\n      log('[transport:manager] → TunnelTransport (no LAN URL)');\n      return tunnelTransport;\n    }\n\n    const lanTransport = new LanHttpTransport(rpcUrl, LAN_RACE_TIMEOUT_MS);","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/app/src/services/transport/TransportManager.ts#L101-L137","documentation":"Tunnel authentication needs either an established sessionToken or the one-time pairingToken (sessionToken ?? pairingToken). With neither, TunnelTransport cannot authenticate to the backend socket relay, so the manager aborts before connecting.","triggerScenarios":"A tunnel profile whose pairingToken was already consumed by a previous successful pairing (they are single-use) and whose sessionToken was never persisted — e.g. the client crashed between server-side pairing completion and storing the session token.","commonSituations":"Crash between pairing-token exchange and session persistence; session token expired or cleared; restored backup carrying stale tokens.","solutions":["Re-pair the device to obtain a fresh pairingToken/sessionToken pair","If the sessionToken exists but is rejected later, route through re-auth rather than reusing the profile","Persist the session token atomically with pairing completion so the two cannot diverge"],"exampleFix":"// before\nconst tm = createTransportManager(profile, opts);\n\n// after\nif (profile.kind === 'tunnel' && !(profile.sessionToken ?? profile.pairingToken)) {\n  await rePairDevice(profile.id); // yields fresh tokens\n}\nconst tm = createTransportManager(profile, opts);","handlingStrategy":"validation","validationCode":"function assertTunnelAuthPresent(p: ConnectionProfile): void {\n  if (p.kind === 'tunnel' && !(p.sessionToken ?? p.pairingToken)) {\n    throw new Error(`profile ${p.id}: no session or pairing token — re-pair the device`);\n  }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Remember pairing tokens are single-use — never persist a profile with a consumed token and no session token","Persist the session token atomically with pairing completion","Route expired sessions through re-auth instead of reusing stale profiles"],"tags":["transport","tunnel","pairing","auth","session"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}