stablyai/orca · error

relay credential resolved as an unexpected credential kind

Error message

relay credential resolved as an unexpected credential kind

What it means

Thrown in MobileRelayE2eeLink.acceptHello when the hello succeeded (ok:true) but parsed.data.credentialKind ('invite'|'resume') does not equal this.options.expectedCredentialKind. The cell resolved the credential as a different kind than the client declared.

Source

Thrown at mobile/src/transport/mobile-relay-e2ee-link.ts:124

  private acceptHello(raw: unknown): void {
    if (typeof raw !== 'string') {
      throw new Error('expected plaintext relay hello')
    }
    let value: unknown
    try {
      value = JSON.parse(raw)
    } catch {
      throw new Error('invalid relay hello JSON')
    }
    const parsed = RelayPhoneHelloSchema.safeParse(value)
    if (!parsed.success) {
      throw new Error('invalid relay hello')
    }
    if (!parsed.data.ok) {
      throw new RelayOuterError(parsed.data.code)
    }
    if (parsed.data.credentialKind !== this.options.expectedCredentialKind) {
      throw new Error('relay credential resolved as an unexpected credential kind')
    }
    this.outerReady = true
    this.options.onHello?.(parsed.data)
    this.channel.start()
  }

  private fail(error: Error): void {
    if (this.closed) {
      return
    }
    this.closed = true
    this.channel.dispose()
    this.options.onError(error)
    this.socket.close()
  }
}

function relaySocketUrl(endpoint: { cellUrl: string; relayHostId: string }): string {

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Verify the credential string and expectedCredentialKind agree at the MobileRelayE2eeLink construction site.
  2. If the host already installed the credential, switch to the resume flow (expectedCredentialKind:'resume' with the resume token).
  3. Regenerate a credential of the correct kind from the host if the stored one was promoted.

Example fix

// before
new MobileRelayE2eeLink({
  credential: resumeToken,
  expectedCredentialKind: 'invite', // wrong: token is a resume token
  ...
})
// after
new MobileRelayE2eeLink({
  credential: resumeToken,
  expectedCredentialKind: 'resume',
  ...
})
Defensive patterns

Strategy: validation

Validate before calling

function assertCredentialKind(credential: string, expectedCredentialKind: 'invite' | 'resume'): void {
  // invite tokens and resume tokens are both 43-char base64url; the caller must
  // track which one it holds. Assert the option matches the tracked provenance.
  if (!credential) throw new Error('missing credential')
  // expectedCredentialKind is the caller's source of truth, not derivable from the token
}

Try / catch

try {
  const link = new MobileRelayE2eeLink(options)
} catch (error) {
  if (error instanceof Error && error.message === 'relay credential resolved as an unexpected credential kind') {
    // re-evaluate whether the host already installed; switch to resume flow
  }
}

Prevention

When it happens

Trigger: The caller passed an invite token but set expectedCredentialKind:'resume', or passed a resume token while the option says 'invite'; the cell internally promoted an invite to resume after install completed.

Common situations: State-machine bug at the call site mislabeling the credential kind; pairing already completed on the host so the invite is no longer interpreted as invite; credential rotated between save and connect.

Related errors


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