{"record":{"id":"0980055f43c6afc1","repo":"stablyai/orca","slug":"relay-credential-rotation-endpoint-state-missing","errorCode":null,"errorMessage":"relay credential rotation endpoint state missing","messagePattern":"relay credential rotation endpoint state missing","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"mobile/src/transport/mobile-relay-credential-rotation.ts","lineNumber":69,"sourceCode":"    const response = await args.client.sendRequest('pairing.provisionRelay', {\n      reqId: pending.reqId,\n      newResumeTokenHash: pending.hash,\n      expectedCurrentHash: bundle.current.hash\n    })\n    if (!response.ok) {\n      throw new Error(`${response.error.code}: ${response.error.message}`)\n    }\n    const installed = DeviceCredentialInstalledSchema.parse(response.result)\n    endpoints = await getEndpoints(args.client, pending.reqId)\n    if (\n      endpoints.installStatus?.state !== 'committed' ||\n      JSON.stringify(endpoints.installStatus.result) !== JSON.stringify(installed)\n    ) {\n      throw new Error('relay credential rotation was not authoritatively committed')\n    }\n  }\n  if (!endpoints.relay || endpoints.installStatus?.state !== 'committed') {\n    throw new Error('relay credential rotation endpoint state missing')\n  }\n  const installed = endpoints.installStatus.result\n  const next = MobileRelayCredentialBundleSchema.parse({\n    ...bundle,\n    current: {\n      token: pending.token,\n      hash: pending.hash,\n      version: installed.currentVersion,\n      expiresAt: installed.resumeExpiresAt\n    },\n    ...(installed.graceExpiresAt\n      ? { grace: { ...bundle.current, expiresAt: installed.graceExpiresAt } }\n      : { grace: undefined }),\n    pending: undefined\n  })\n  await args.writeBundle(next)\n  return { bundle: next, relay: endpoints.relay }\n}","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/mobile/src/transport/mobile-relay-credential-rotation.ts#L51-L87","documentation":"Thrown near the end of `rotateMobileRelayCredential` when `endpoints.relay` is missing or the install is not in `committed` state. Even if `provisionRelay` succeeded, the rotation cannot complete without an authoritative relay endpoint to publish alongside the credential.","triggerScenarios":"Server returned `committed` install status but no `relay` endpoint object; install reverted to a non-committed state between the two `getEndpoints` calls; server allocated the credential but not the relay cell.","commonSituations":"Partial server-side provisioning; a relay cell that was deallocated; misconfigured relay that reports install success without a routable endpoint.","solutions":["Retry `getEndpoints` after a delay to allow the relay cell to be allocated.","If `relay` is persistently absent, surface a relay-unavailable error to the user and keep the prior credential.","Verify with the server operator that relay provisioning is healthy."],"exampleFix":"// before\n// proceeds to writeBundle with undefined relay -> throws\n\n// after\nfor (let i = 0; i < 3 && (!endpoints.relay || endpoints.installStatus?.state !== 'committed'); i++) {\n  await delay(500)\n  endpoints = await getEndpoints(args.client, pending.reqId)\n}\nif (!endpoints.relay) throw new Error('relay endpoint never allocated')","handlingStrategy":"retry","validationCode":"// Probe for relay presence before promoting:\nconst endpoints = await getEndpoints(client, pending.reqId)\nif (!endpoints.relay) { await delay(500); /* re-check */ }","typeGuard":"function hasRelayEndpoint(e: { relay?: unknown }): boolean { return !!e.relay }","tryCatchPattern":"try { return await rotateMobileRelayCredential(args) } catch (e) { if (e.message === 'relay credential rotation endpoint state missing') { scheduleRetry(args); return null } throw e }","preventionTips":["Retry getEndpoints a few times to allow relay cell allocation.","Keep the previous credential intact until the new one is fully confirmed.","Surface relay-unavailable distinctly so users retry later."],"tags":["relay","consistency","credentials","rotation"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}