BloopAI/vibe-kanban · error

Server message has invalid side identifier.

Error message

Server message has invalid side identifier.

What it means

finishSpake2Enrollment requires the first byte of the 33-byte server message to be 0x42 ('B', the server side identifier); any other tag byte throws. This detects messages from the wrong PAKE side or a foreign protocol.

Source

Thrown at packages/web-core/src/shared/lib/relayPake.ts:81

      passwordBytes,
      passwordScalar,
      xScalar,
      clientMessageBytes: clientPointBytes,
    },
    clientMessageB64: bytesToBase64(clientMessage),
  };
}

export async function finishSpake2Enrollment(
  state: Spake2EnrollmentClientState,
  serverMessageB64: string
): Promise<Uint8Array> {
  const serverMessage = base64ToBytes(serverMessageB64);
  if (serverMessage.length !== 33) {
    throw new Error('Server message has invalid length.');
  }
  if (serverMessage[0] !== 0x42) {
    throw new Error('Server message has invalid side identifier.');
  }

  const serverPointBytes = serverMessage.slice(1);
  const serverPoint = ed25519.ExtendedPoint.fromHex(serverPointBytes);
  const negativePasswordScalar =
    (CURVE_ORDER - state.passwordScalar) % CURVE_ORDER;

  const keyPoint = serverPoint
    .add(SPAKE2_N.multiply(negativePasswordScalar))
    .multiply(state.xScalar);
  const keyPointBytes = keyPoint.toRawBytes();

  return hashAb(
    state.passwordBytes,
    SPAKE2_CLIENT_ID,
    SPAKE2_SERVER_ID,
    state.clientMessageBytes,
    serverPointBytes,

View on GitHub (pinned to 4deb7eca8f)

Solutions

  1. Confirm the value passed is the SERVER's response message, not the client's clientMessageB64.
  2. Hard-refresh the web app to clear a stale cached bundle that disagrees with the server's protocol version.
  3. Check the relay server's SPAKE2 implementation writes 0x42 as the side byte.
  4. Log serverMessage[0] and compare against expected 0x42 to identify what the server actually sent.

Example fix

// before
const key = await finishSpake2Enrollment(state, echoBackMessage);
// after
const resp = await startRelaySpake2Enrollment(hostId, sessionId, req); // server message comes from THIS response
const key = await finishSpake2Enrollment(state, resp.serverMessageB64);
Defensive patterns

Strategy: validation

Validate before calling

function hasServerSideTag(b64: string): boolean {
  try {
    const raw = atob(b64);
    return raw.length === 33 && raw.charCodeAt(0) === 0x42;
  } catch { return false; }
}

Type guard

function isServerSideMessage(msg: Uint8Array): boolean {
  return msg.length === 33 && msg[0] === 0x42; // 0x42 = 'B' (server side)
}

Try / catch

try {
  const key = await finishSpake2Enrollment(state, serverMessageB64);
} catch (e) {
  if (e instanceof Error && e.message.includes('side identifier')) {
    console.error('Got a non-server (0x41 client?) message; check message routing.');
    restartPairing();
  }
}

Prevention

When it happens

Trigger: The decoded server message is 33 bytes but its first byte is not 0x42 — e.g. a client message (0x41) echoed back, a different protocol framing byte from a mismatched server version, or corrupted data that coincidentally has 33 bytes.

Common situations: Point-to-point plumbing bugs where the client's own message is fed back as the server response; relay forwarding messages out of order or between two pairing sessions; server upgraded to a new message tag while the browser bundle is stale/cached.

Related errors


AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29). Data as JSON: /api/errors/93c6db047cdcf2c2. Report an issue: GitHub.