{"record":{"id":"2ffc35ad84308032","repo":"stablyai/orca","slug":"e2ee-v2-ready-has-not-been-accepted","errorCode":null,"errorMessage":"E2EE v2 ready has not been accepted","messagePattern":"E2EE v2 ready has not been accepted","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"mobile/src/transport/mobile-e2ee-v2-client-session.ts","lineNumber":79,"sourceCode":"\n  acceptReady(ready: unknown): boolean {\n    const handshake = validateMobileE2EEV2Handshake(this.hello, ready)\n    if (!handshake || !equalBytes(handshake.desktopPublicKey, this.pinnedDesktopPublicKey)) {\n      return false\n    }\n    this.schedule = deriveMobileE2EEV2KeySchedule({\n      sharedSecret: deriveSharedKey(this.clientSecretKey, this.pinnedDesktopPublicKey),\n      transcript: encodeMobileE2EEV2Transcript(handshake),\n      clientNonce: handshake.clientNonce,\n      desktopNonce: handshake.desktopNonce\n    })\n    this.transcriptHashB64Value = encodeBase64(this.schedule.transcriptHash)\n    return true\n  }\n\n  get transcriptHashB64(): string {\n    if (!this.transcriptHashB64Value) {\n      throw new Error('E2EE v2 ready has not been accepted')\n    }\n    return this.transcriptHashB64Value\n  }\n\n  openText(frameB64: string): string | null {\n    const frame = decodeCanonicalBase64(frameB64)\n    if (!frame) {\n      return null\n    }\n    const plaintext = this.open(frame, 'text')\n    return plaintext ? new TextDecoder().decode(plaintext) : null\n  }\n\n  openBinary(frame: Uint8Array): Uint8Array | null {\n    return this.open(frame, 'binary')\n  }\n\n  sealText(plaintext: string): string {","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/mobile/src/transport/mobile-e2ee-v2-client-session.ts#L61-L97","documentation":"Thrown by the `transcriptHashB64` getter on `MobileE2EEV2ClientSession` when the key schedule has not been derived yet. The getter is only meaningful after `acceptReady()` has successfully installed `this.schedule` and cached `transcriptHashB64Value`. Reaching the throw means the property was read before the v2 ready handshake completed (or before `acceptReady` returned true).","triggerScenarios":"Calling `session.transcriptHashB64` before `session.acceptReady(ready)` returned `true`. This includes reading it inside a constructor, in a test that skipped the handshake step, or from any code path that races the getter against the awaiting-ready state.","commonSituations":"Unit tests that construct a session via `MobileE2EEV2ClientSession.create(...)` and assert on `transcriptHashB64` without first feeding a valid desktop ready message; refactors that moved the transcript-hash read upstream of `acceptReady`; a caller that ignores the boolean return of `acceptReady` and proceeds as if accepted.","solutions":["Ensure `acceptReady(ready)` was called with a valid ready payload and returned `true` before reading `transcriptHashB64`.","Gate the read on the physical channel's state machine — only the `awaiting-authenticated` branch in `MobileE2EEV2PhysicalChannel.acceptReady` is allowed to read it.","In tests, drive the session through `acceptReady` with a handshake whose `desktopPublicKey` equals the pinned key, or expose a test helper that derives the schedule deterministically."],"exampleFix":"// before\nconst session = MobileE2EEV2ClientSession.create({ desktopPublicKeyB64, transport: 'relay' })\nconst hash = session.transcriptHashB64 // throws\n\n// after\nif (!session.acceptReady(validReadyPayload)) {\n  throw new Error('handshake rejected')\n}\nconst hash = session.transcriptHashB64","handlingStrategy":"validation","validationCode":"// Track acceptance externally; only read the getter after acceptReady returns true.\nconst accepted = session.acceptReady(readyPayload)\nif (!accepted) return // do not read transcriptHashB64\nconst hash = session.transcriptHashB64","typeGuard":"// No public isAccepted() exists; derive it from the side effect:\nfunction sessionAccepted(session: MobileE2EEV2ClientSession): boolean {\n  try { void session.transcriptHashB64; return true } catch { return false }\n}","tryCatchPattern":"try { const hash = session.transcriptHashB64 } catch (e) { if (e.message === 'E2EE v2 ready has not been accepted') { /* defer this read until onAuthenticated */ } else throw e }","preventionTips":["Read transcriptHashB64 only inside the awaiting-authenticated branch of the physical channel.","Treat the boolean return of acceptReady as authoritative — never proceed on false.","In tests, drive the full handshake before asserting on the hash."],"tags":["e2ee","state-machine","crypto","handshake"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}