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

  1. Always construct TunnelTransport via new (the constructor guarantees the keypair) — never reuse a torn-down instance
  2. If you pass devicePrivkeyB64, verify it is valid base64url of a 32-byte seed before constructing so keypairFromSecretKey cannot throw mid-init
  3. 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

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


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)