stablyai/orca · error · Error
Expected plaintext E2EE v2 ready
Error message
Expected plaintext E2EE v2 ready
What it means
Thrown by `acceptReady` in `MobileE2EEV2PhysicalChannel` when the very first inbound frame (the desktop's ready message) is not a string. The ready frame is required to arrive as plaintext JSON text because the key schedule does not exist yet — binary cannot be decrypted. A non-string payload means the transport is delivering binary (ArrayBuffer/Blob) where text was negotiated.
Source
Thrown at mobile/src/transport/mobile-e2ee-v2-physical-channel.ts:124
if (this.state === 'awaiting-authenticated') {
if (typeof plaintext === 'string' && isAuthenticationRejection(plaintext)) {
throw new MobileE2EEAuthenticationError()
}
if (typeof plaintext !== 'string' || !this.isAuthenticated(plaintext)) {
throw new Error('Invalid E2EE v2 authenticated response')
}
this.state = 'ready'
this.args.onAuthenticated()
} else if (typeof plaintext === 'string') {
this.args.onText(plaintext)
} else {
this.args.onBinary(plaintext)
}
}
private acceptReady(raw: unknown): void {
if (typeof raw !== 'string') {
throw new Error('Expected plaintext E2EE v2 ready')
}
let ready: unknown
try {
ready = JSON.parse(raw)
} catch {
throw new Error('Invalid E2EE v2 ready JSON')
}
if (!this.args.session.acceptReady(ready)) {
throw new Error('Invalid E2EE v2 ready')
}
this.state = 'awaiting-authenticated'
this.outboundQueue.enqueue({
kind: 'text',
plaintext: JSON.stringify({
type: 'e2ee_auth',
v: 2,
transcriptHashB64: this.args.session.transcriptHashB64,
deviceToken: this.args.deviceTokenView on GitHub (pinned to 1136503c6a)
Solutions
- Ensure the underlying WebSocket is in text mode for the handshake — set `socket.binaryType = 'blob'` only after the ready exchange, or have the desktop send the ready frame as text.
- If using a custom transport, coerce the first frame to a string before calling `channel.handleMessage`.
- In tests, pass a JSON string for the ready message, not an encoded buffer.
Example fix
// before
socket.onmessage = (e) => channel.handleMessage(e.data) // e.data is ArrayBuffer -> throws
// after
socket.onmessage = (e) => {
const raw = e.data instanceof ArrayBuffer ? new TextDecoder().decode(e.data) : e.data
channel.handleMessage(raw)
} Defensive patterns
Strategy: validation
Validate before calling
function isTextFrame(raw: unknown): raw is string { return typeof raw === 'string' }
// Before forwarding to the channel:
if (!isTextFrame(firstFrame)) { socket.close(); return } Type guard
function isReadyText(raw: unknown): raw is string { return typeof raw === 'string' } Try / catch
try { channel.handleMessage(firstFrame) } catch (e) { if (e.message === 'Expected plaintext E2EE v2 ready') { /* configure socket for text mode, reconnect */ } else throw e } Prevention
- Set WebSocket binaryType appropriately and ensure the handshake frame arrives as text.
- Coerce ArrayBuffer first frames to string at the transport boundary.
- In tests, send the ready frame as a JSON string.
When it happens
Trigger: WebSocket `onmessage` delivered an `ArrayBuffer`/`Blob` as the first frame; relay or desktop misconfiguration that sends binary framing from the start; the `decodeBinary` path being invoked before the channel expects it.
Common situations: A relay implementation that always frames as binary; a browser/React-Native WebSocket defaulting to arraybuffer type when the desktop assumed text; a test harness that feeds a `Uint8Array` instead of a JSON string.
Related errors
- expected plaintext relay hello
- E2EE v2 ready has not been accepted
- Invalid E2EE v2 authenticated response
- Invalid E2EE v2 ready JSON
- Invalid E2EE v2 ready
AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12).
Data as JSON: /api/errors/b897e2e8e9b5d163.
Report an issue: GitHub.