{"record":{"id":"b897e2e8e9b5d163","repo":"stablyai/orca","slug":"expected-plaintext-e2ee-v2-ready","errorCode":null,"errorMessage":"Expected plaintext E2EE v2 ready","messagePattern":"Expected plaintext E2EE v2 ready","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"mobile/src/transport/mobile-e2ee-v2-physical-channel.ts","lineNumber":124,"sourceCode":"    if (this.state === 'awaiting-authenticated') {\n      if (typeof plaintext === 'string' && isAuthenticationRejection(plaintext)) {\n        throw new MobileE2EEAuthenticationError()\n      }\n      if (typeof plaintext !== 'string' || !this.isAuthenticated(plaintext)) {\n        throw new Error('Invalid E2EE v2 authenticated response')\n      }\n      this.state = 'ready'\n      this.args.onAuthenticated()\n    } else if (typeof plaintext === 'string') {\n      this.args.onText(plaintext)\n    } else {\n      this.args.onBinary(plaintext)\n    }\n  }\n\n  private acceptReady(raw: unknown): void {\n    if (typeof raw !== 'string') {\n      throw new Error('Expected plaintext E2EE v2 ready')\n    }\n    let ready: unknown\n    try {\n      ready = JSON.parse(raw)\n    } catch {\n      throw new Error('Invalid E2EE v2 ready JSON')\n    }\n    if (!this.args.session.acceptReady(ready)) {\n      throw new Error('Invalid E2EE v2 ready')\n    }\n    this.state = 'awaiting-authenticated'\n    this.outboundQueue.enqueue({\n      kind: 'text',\n      plaintext: JSON.stringify({\n        type: 'e2ee_auth',\n        v: 2,\n        transcriptHashB64: this.args.session.transcriptHashB64,\n        deviceToken: this.args.deviceToken","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/mobile/src/transport/mobile-e2ee-v2-physical-channel.ts#L106-L142","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nsocket.onmessage = (e) => channel.handleMessage(e.data) // e.data is ArrayBuffer -> throws\n\n// after\nsocket.onmessage = (e) => {\n  const raw = e.data instanceof ArrayBuffer ? new TextDecoder().decode(e.data) : e.data\n  channel.handleMessage(raw)\n}","handlingStrategy":"validation","validationCode":"function isTextFrame(raw: unknown): raw is string { return typeof raw === 'string' }\n// Before forwarding to the channel:\nif (!isTextFrame(firstFrame)) { socket.close(); return }","typeGuard":"function isReadyText(raw: unknown): raw is string { return typeof raw === 'string' }","tryCatchPattern":"try { channel.handleMessage(firstFrame) } catch (e) { if (e.message === 'Expected plaintext E2EE v2 ready') { /* configure socket for text mode, reconnect */ } else throw e }","preventionTips":["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."],"tags":["e2ee","handshake","websocket","framing"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}