{"record":{"id":"efd86df7c999fa33","repo":"tinyhumansai/openhuman","slug":"tunnel-no-device-keypair","errorCode":null,"errorMessage":"[tunnel] no device keypair","messagePattern":"\\[tunnel\\] no device keypair","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"app/src/services/transport/TunnelTransport.ts","lineNumber":157,"sourceCode":"        this.clientEphemeralKeypair = null;\n        this.cipher = null;\n        this._connectPromise = null;\n      });\n\n      socket.on('connect_error', (err: Error) => {\n        logErr('[tunnel] connect_error %s', err.message);\n        reject(err);\n        this._connectPromise = null;\n      });\n    });\n\n    return this._connectPromise;\n  }\n\n  // -- handshake -------------------------------------------------------------\n\n  private async sendHandshake(): Promise<void> {\n    if (!this.deviceKeypair) throw new Error('[tunnel] no device keypair');\n\n    const corePubkey = base64urlDecode(this.corePubkeyB64);\n    const devicePubkeyB64 = base64urlEncode(this.deviceKeypair.publicKey);\n    const clientEphemeral = generateKeypair();\n    const clientEphemeralPubkeyB64 = base64urlEncode(clientEphemeral.publicKey);\n\n    const payload = new TextEncoder().encode(\n      JSON.stringify({\n        device_pubkey: devicePubkeyB64,\n        client_ephemeral_pubkey: clientEphemeralPubkeyB64,\n      })\n    );\n\n    // Seal the handshake payload to the core's public key.\n    const handshakeFrame = sealHandshake(corePubkey, payload);\n    const frameB64 = base64urlEncode(handshakeFrame);\n\n    log('[tunnel] sending sealed handshake frame_len=%d', handshakeFrame.length);","sourceCodeStart":139,"sourceCodeEnd":175,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/app/src/services/transport/TunnelTransport.ts#L139-L175","documentation":"TunnelTransport.sendHandshake needs the device's long-term keypair to prove identity during the XChaCha20-Poly1305 handshake. This fires when connect() reached the handshake stage without a keypair — none was supplied to the constructor (the manager passes profile.devicePrivkey) or it was never generated/loaded.","triggerScenarios":"Constructing TunnelTransport from a tunnel profile lacking devicePrivkey and calling connect(); or a code path invoking sendHandshake before device key generation completed (e.g. test harness passing only token arguments).","commonSituations":"Tunnel profile persisted without the device private key (pairing never fully completed); tests building TunnelTransport directly with positional args and omitting the key.","solutions":["Complete device pairing so devicePrivkey is stored with the profile (keyring/security-devices flow)","When constructing TunnelTransport manually, generate/load the device keypair first and pass it in","Refuse to build a kind 'tunnel' profile without devicePrivkey at validation time"],"exampleFix":"// before\nconst t = new TunnelTransport(url, channelId, corePubkey, token, undefined, 'pairing');\nawait t.connect();\n\n// after\nconst devicePrivkey = await loadOrCreateDeviceKey(); // Uint8Array from keyring\nconst t = new TunnelTransport(url, channelId, corePubkey, token, devicePrivkey, 'pairing');\nawait t.connect();","handlingStrategy":"validation","validationCode":"function tunnelProfileHasDeviceKey(p: ConnectionProfile): boolean {\n  return p.kind !== 'tunnel' || !!p.devicePrivkey;\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Complete device pairing (key generation + storage) before any tunnel profile is marked usable","When constructing TunnelTransport directly, load-or-create the device keypair first","Fail profile validation when devicePrivkey is absent on a tunnel profile"],"tags":["transport","tunnel","crypto","keypair","pairing"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}