stablyai/orca · critical · MobileE2EEAuthenticationError

E2EE device authentication rejected

Error message

E2EE device authentication rejected

What it means

`MobileE2EEAuthenticationError` is thrown by the physical channel when, in the `awaiting-authenticated` state, the decrypted plaintext is a JSON message with `type === 'e2ee_error'` (detected by `isAuthenticationRejection`). That is the desktop's explicit signal that the mobile device's `e2ee_auth` payload — device token or transcript hash — was rejected. It is a distinct, named error class so callers can branch on it rather than string-matching.

Source

Thrown at mobile/src/transport/mobile-e2ee-v2-physical-channel.ts:108

  private async processMessage(raw: unknown, generation: number): Promise<void> {
    if (generation !== this.generation) {
      return
    }
    if (this.state === 'awaiting-ready') {
      this.acceptReady(raw)
      return
    }

    const plaintext =
      typeof raw === 'string'
        ? this.args.session.openText(raw)
        : await this.openBinary(raw, generation)
    if (generation !== this.generation || plaintext === null) {
      return
    }
    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

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Catch `MobileE2EEAuthenticationError` specifically in the transport error handler and surface a re-pair flow rather than retrying.
  2. If the device token may be stale, clear the stored credential bundle and re-run pairing.
  3. Verify the desktop is running a protocol-compatible version (v2 e2ee) before retry.

Example fix

// before
try { link.sendText('hello') } catch (e) { retry() } // wrong: auth errors are not retried

// after
try {
  // ...
} catch (e) {
  if (e instanceof MobileE2EEAuthenticationError) {
    await clearDeviceTokenAndStartRepair(hostId)
  } else {
    throw e
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// No pre-call validation: the rejection is the desktop's response. Detect after the fact.
// Ensure onError handler branches on the error class.

Type guard

function isAuthRejection(e: unknown): e is MobileE2EEAuthenticationError { return e instanceof MobileE2EEAuthenticationError }

Try / catch

channel.onError = (e) => { if (e instanceof MobileE2EEAuthenticationError) { startRepairFlow(hostId) } else { reportTransient(e) } }

Prevention

When it happens

Trigger: Desktop replied to `e2ee_auth` with `{ "type": "e2ee_error", ... }`; the device token presented in `e2ee_auth` is unknown, expired, or revoked on the desktop side; the transcript hash did not match the desktop's view of the handshake transcript.

Common situations: A host that was unpaired or reset while the mobile still held a stale device token; desktop-side rollback to a state that does not recognize the device; user revoked the device from the desktop UI between pairing and reconnect.

Understand the failure class

Related errors


AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12). Data as JSON: /api/errors/1f611b18793b465b. Report an issue: GitHub.