{"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":null,"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/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/services/transport/TunnelTransport.ts#L139-L175","documentation":"sendHandshake() is an invariant guard: it needs this.deviceKeypair to seal the first frame (device pubkey + client ephemeral pubkey encrypted to the core's static key). In the shipped constructor the keypair is always set — restored from devicePrivkeyB64 via keypairFromSecretKey, or freshly generated — so in practice this fires only on a mis-constructed instance (construction threw midway and was swallowed, a partial subclass, or refactoring removed the assignment).","triggerScenarios":"Someone constructs TunnelTransport-like objects without the constructor (Object.create, deserialization); keypairFromSecretKey(base64urlDecode(priv)) throwing earlier and the error caught, leaving a half-built object whose methods are then called; future code paths that null deviceKeypair on teardown like the disconnect handler does for cipher.","commonSituations":"Almost exclusively a programming bug or an exotic serialization path, not a runtime/environment condition.","solutions":["Always construct TunnelTransport via new (the constructor guarantees the keypair) — never reuse a torn-down instance","If you pass devicePrivkeyB64, verify it is valid base64url of a 32-byte seed before constructing so keypairFromSecretKey cannot throw mid-init","Treat hitting this in production as a bug: capture a stack and file it rather than retrying"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// If you supply a device key, validate it before construction so init cannot half-complete:\nfunction isValidBase64urlSeed(s: string): boolean {\n  return /^[A-Za-z0-9_-]{43}$/.test(s); // 32 bytes, no padding\n}","typeGuard":null,"tryCatchPattern":"try {\n  await tunnel.call(method, params);\n} catch (err) {\n  if (err instanceof Error && err.message === '[tunnel] no device keypair') {\n    // invariant bug — rebuild the transport via new TunnelTransport(...), never reuse this instance\n  }\n  throw err;\n}","preventionTips":["Only obtain TunnelTransport through its constructor or TransportManager","Never catch-and-continue around construction; a half-built instance throws this later","Treat occurrences as a defect to report, not a runtime condition to handle"],"tags":["tunnel","crypto","handshake","invariant","keypair"],"backgroundTag":"missing-crypto-key","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}