stablyai/orca · error · Error

relay credential rotation pending state missing

Error message

relay credential rotation pending state missing

What it means

Thrown in `rotateMobileRelayCredential` when `bundle.pending` is missing after the block that creates and persists it. The preceding code writes `bundle` with a `pending` field via the schema-strict parse, so reaching this throw implies the schema stripped `pending` (e.g. the parser set it to undefined because the strict object rejected it) or a caller passed a pre-rotated bundle whose `pending` was already consumed. It is a defensive invariant guard.

Source

Thrown at mobile/src/transport/mobile-relay-credential-rotation.ts:47

  if (!bundle.pending) {
    const randomBytes = args.randomBytes ?? ExpoCrypto.getRandomBytes
    const token = encodeBase64Url(randomBytes(32))
    bundle = MobileRelayCredentialBundleSchema.parse({
      ...bundle,
      pending: {
        token,
        hash: hashMobileRelayCredential(token),
        reqId: `rotate-${encodeBase64Url(randomBytes(16))}`
      }
    })
    // Why: a crash or lost response must leave enough material to query the
    // one global install key before any second authorization attempt.
    await args.writeBundle(bundle)
  }

  const pending = bundle.pending
  if (!pending) {
    throw new Error('relay credential rotation pending state missing')
  }
  let endpoints = await getEndpoints(args.client, pending.reqId)
  if (endpoints.installStatus?.state !== 'committed') {
    const response = await args.client.sendRequest('pairing.provisionRelay', {
      reqId: pending.reqId,
      newResumeTokenHash: pending.hash,
      expectedCurrentHash: bundle.current.hash
    })
    if (!response.ok) {
      throw new Error(`${response.error.code}: ${response.error.message}`)
    }
    const installed = DeviceCredentialInstalledSchema.parse(response.result)
    endpoints = await getEndpoints(args.client, pending.reqId)
    if (
      endpoints.installStatus?.state !== 'committed' ||
      JSON.stringify(endpoints.installStatus.result) !== JSON.stringify(installed)
    ) {
      throw new Error('relay credential rotation was not authoritatively committed')

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Confirm the bundle passed in either already has a valid `pending` or no `pending` (in which case rotation creates one).
  2. If this fires, suspect a schema regression in `MobileRelayCredentialBundleSchema` around the `pending` field.
  3. Add a regression test that asserts `pending` is present after the create-and-write block.

Example fix

// before
const bundle = await readBundle() // pending undefined unexpectedly
await rotateMobileRelayCredential({ client, bundle, writeBundle })

// after
if (!bundle.pending && !bundle.current) throw new Error('bundle has neither pending nor current')
// the rotation function itself is the correct path; this throw indicates corruption upstream
Defensive patterns

Strategy: validation

Validate before calling

function bundleHasPending(b: MobileRelayCredentialBundle): boolean { return !!b.pending }
if (!bundleHasPending(bundle) && !bundle.current) { throw new Error('bundle corrupted') }

Type guard

function hasPending(b: MobileRelayCredentialBundle): b is MobileRelayCredentialBundle & { pending: NonNullable<MobileRelayCredentialBundle['pending']> } { return !!b.pending }

Prevention

When it happens

Trigger: Caller passes a bundle where `pending` was set to `undefined` in a way that survived schema parse; `writeBundle` mutated the in-memory object; a schema change that made `pending` non-optional in the wrong direction.

Common situations: Internal logic error or future refactor that drops the pending write; extremely unlikely under normal control flow because the constructor block always sets `pending`.

Related errors


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