{"record":{"id":"9640cb85aaf259eb","repo":"moeru-ai/airi","slug":"listaccounts-failed","errorCode":null,"errorMessage":"listAccounts failed","messagePattern":"listAccounts failed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/composables/use-linked-accounts.ts","lineNumber":162,"sourceCode":"  )\n\n  /**\n   * Client-side mirror of better-auth's `FAILED_TO_UNLINK_LAST_ACCOUNT`\n   * guard so we can surface a user-friendly message before round-tripping.\n   */\n  function isLastSignInMethod(providerId: string): boolean {\n    if (providerId === 'credential')\n      return socialLinkedCount.value === 0\n    return !hasCredentialAccount.value && socialLinkedCount.value <= 1\n  }\n\n  async function refresh() {\n    loading.value = true\n    error.value = null\n    try {\n      const { data, error: apiError } = await args.client.listAccounts()\n      if (apiError)\n        throw new Error(apiError.message ?? 'listAccounts failed')\n      // better-auth 1.6.6 widens listAccounts elements to `any`; consume\n      // the row directly rather than dressing `any` up with a fake shape.\n      // Field layout: node_modules/better-auth/dist/api/routes/account.mjs L20-50.\n      linkedAccounts.value = (data ?? []).map(account => ({\n        id: account.id,\n        accountId: account.accountId,\n        providerId: account.providerId,\n        createdAt: account.createdAt instanceof Date\n          ? account.createdAt.toISOString()\n          : account.createdAt,\n        scopes: account.scopes ?? [],\n      }))\n      loaded.value = true\n    }\n    catch (err) {\n      // Keep prior `linkedAccounts` on error so a transient 5xx doesn't\n      // flip `hasCredentialAccount` and mis-route the password UI.\n      // Source: PR #1753 review (chatgpt-codex-connector P2).","sourceCodeStart":144,"sourceCodeEnd":180,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/packages/stage-ui/src/composables/use-linked-accounts.ts#L144-L180","documentation":"Thrown inside refresh() when the better-auth typed client's listAccounts() resolves with a non-null `error` object. The composable only falls back to the generic 'listAccounts failed' string when the server error carried no message. It surfaces backend auth-service failures to the UI error ref rather than silently showing an empty list.","triggerScenarios":"args.client.listAccounts() returns { data: null, error: { ... } }. This happens when the auth backend returns non-2xx (5xx outage, 401 expired session cookie, 403 forbidden), the network request fails and better-auth wraps it, or the auth service URL is misconfigured/unreachable.","commonSituations":"Auth service is down or restarting. The user's session cookie expired and silent refresh also failed (stage-web Bearer token revoked). The better-auth base URL env var points at the wrong host. CORS blocks the request in stage-web.","solutions":["Check the auth service health and the network tab: the real status/message is in apiError.message — inspect `error.value` in the composable.","If the session expired, re-authenticate so the credentialed/Bearer client regains a valid session, then call refresh() again.","Verify better-auth base URL and CORS configuration allow the requesting origin.","Treat transient 5xx as retryable: the composable intentionally keeps prior linkedAccounts on error, so a manual refresh() after the outage recovers."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// refresh() already clears error and sets loading; pre-check auth before mount\nif (!args.isAuthenticated.value) {\n  // skip refresh; listAccounts requires an active session\n}","typeGuard":"null","tryCatchPattern":"// already implemented in refresh(): error surfaced via error.value, prior list kept\ntry {\n  const { data, error: apiError } = await args.client.listAccounts()\n  if (apiError) throw new Error(apiError.message ?? 'listAccounts failed')\n  // ...\n}\ncatch (err) {\n  error.value = args.describeError(err) || args.messages.listFailed\n}","preventionTips":["Only call refresh() when isAuthenticated is true.","Keep prior linkedAccounts on error so the UI does not blank out on a transient 5xx.","Treat 401 as a re-auth trigger rather than a permanent failure."],"tags":["network","better-auth","auth","linked-accounts","api-error"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}