{"record":{"id":"abfd7dce43c49e48","repo":"stablyai/orca","slug":"relay-outer-code","errorCode":null,"errorMessage":"relay_outer_${code}","messagePattern":"relay_outer_(.+?)","errorType":"exception","errorClass":"RelayOuterError","httpStatus":null,"severity":"error","filePath":"mobile/src/transport/mobile-relay-e2ee-link.ts","lineNumber":121,"sourceCode":"    this.socket.onclose = (event) => this.fail(new RelayOuterError(event.code || 1006))\n  }\n\n  private acceptHello(raw: unknown): void {\n    if (typeof raw !== 'string') {\n      throw new Error('expected plaintext relay hello')\n    }\n    let value: unknown\n    try {\n      value = JSON.parse(raw)\n    } catch {\n      throw new Error('invalid relay hello JSON')\n    }\n    const parsed = RelayPhoneHelloSchema.safeParse(value)\n    if (!parsed.success) {\n      throw new Error('invalid relay hello')\n    }\n    if (!parsed.data.ok) {\n      throw new RelayOuterError(parsed.data.code)\n    }\n    if (parsed.data.credentialKind !== this.options.expectedCredentialKind) {\n      throw new Error('relay credential resolved as an unexpected credential kind')\n    }\n    this.outerReady = true\n    this.options.onHello?.(parsed.data)\n    this.channel.start()\n  }\n\n  private fail(error: Error): void {\n    if (this.closed) {\n      return\n    }\n    this.closed = true\n    this.channel.dispose()\n    this.options.onError(error)\n    this.socket.close()\n  }","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/mobile/src/transport/mobile-relay-e2ee-link.ts#L103-L139","documentation":"RelayOuterError is thrown (message relay_outer_${code}) when the cell hello has ok:false with a code in [4000,4999]. This is the relay cell explicitly rejecting the connect credential supplied in the relay-auth frame.","triggerScenarios":"The cell received an invite or resume credential it refuses: expired invite, already-consumed invite, unknown/revoked resume token, credential version superseded after a host reassignment.","commonSituations":"Invite expired before the user completed pairing; resume token invalidated after host rotation; clock skew making a lease look expired; replaying a credential from a completed/abandoned attempt.","solutions":["Decode the 4xxx code against the relay protocol and map it to user-facing guidance (expired vs revoked vs unknown).","For expired/used invites, generate a fresh invite from the desktop host and restart the connect.","For resume failures, fall back to a fresh pair.","Compare the current time against leaseExpiresAt / resumeExpiresAt from a prior successful hello to catch clock skew."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"import { RelayOuterError } from './mobile-relay-e2ee-link'\nfunction isRelayOuterError(error: unknown): error is RelayOuterError {\n  return error instanceof RelayOuterError\n}","tryCatchPattern":"import { RelayOuterError } from './mobile-relay-e2ee-link'\ntry {\n  const link = new MobileRelayE2eeLink(options)\n} catch (error) {\n  if (error instanceof RelayOuterError) {\n    switch (error.code) {\n      case 4101: // example: expired invite\n        promptRegenerateInvite()\n        break\n      default:\n        showRelayRejection(error.code)\n    }\n  }\n}","preventionTips":["Treat RelayOuterError as the canonical credential-rejection signal and map each 4xxx code to UX.","Check leaseExpiresAt/resumeExpiresAt locally before dialing to pre-empt expiry codes.","Never replay a credential after a 4xxx rejection; obtain a fresh one."],"tags":["relay","websocket","authentication","e2ee","relay-outer-error"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}