tinyhumansai/openhuman · error
[tunnel] no device keypair
Error message
[tunnel] no device keypair
What it means
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).
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
Defensive patterns
Strategy: try-catch
Validate before calling
// If you supply a device key, validate it before construction so init cannot half-complete:
function isValidBase64urlSeed(s: string): boolean {
return /^[A-Za-z0-9_-]{43}$/.test(s); // 32 bytes, no padding
} Try / catch
try {
await tunnel.call(method, params);
} catch (err) {
if (err instanceof Error && err.message === '[tunnel] no device keypair') {
// invariant bug — rebuild the transport via new TunnelTransport(...), never reuse this instance
}
throw err;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: Almost exclusively a programming bug or an exotic serialization path, not a runtime/environment condition.
Related errors
- [tunnel] no session cipher — handshake incomplete
- Ed25519 derivation path must be fully hardened
- encrypted payload too short
- encryption key must decode to 32 raw bytes
- Failed to derive private key for path
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/efd86df7c999fa33.
Report an issue: GitHub.
Appendix: source
Thrown at app/src/services/transport/TunnelTransport.ts:157
this.clientEphemeralKeypair = null;
this.cipher = null;
this._connectPromise = null;
});
socket.on('connect_error', (err: Error) => {
logErr('[tunnel] connect_error %s', err.message);
reject(err);
this._connectPromise = null;
});
});
return this._connectPromise;
}
// -- handshake -------------------------------------------------------------
private async sendHandshake(): Promise<void> {
if (!this.deviceKeypair) throw new Error('[tunnel] no device keypair');
const corePubkey = base64urlDecode(this.corePubkeyB64);
const devicePubkeyB64 = base64urlEncode(this.deviceKeypair.publicKey);
const clientEphemeral = generateKeypair();
const clientEphemeralPubkeyB64 = base64urlEncode(clientEphemeral.publicKey);
const payload = new TextEncoder().encode(
JSON.stringify({
device_pubkey: devicePubkeyB64,
client_ephemeral_pubkey: clientEphemeralPubkeyB64,
})
);
// Seal the handshake payload to the core's public key.
const handshakeFrame = sealHandshake(corePubkey, payload);
const frameB64 = base64urlEncode(handshakeFrame);
log('[tunnel] sending sealed handshake frame_len=%d', handshakeFrame.length);View on GitHub (pinned to 7491200858)