diff --git a/app/src/utils/__tests__/desktopDeepLinkListener.test.ts b/app/src/utils/__tests__/desktopDeepLinkListener.test.ts index 5c6e9b49b..35706be84 100644 --- a/app/src/utils/__tests__/desktopDeepLinkListener.test.ts +++ b/app/src/utils/__tests__/desktopDeepLinkListener.test.ts @@ -10,6 +10,7 @@ import { } from '../../store/deepLinkAuthState'; import { getStoredCoreMode } from '../configPersistence'; import { + authStoreFailureUserMessage, classifyAuthStoreFailure, registerAuthDeepLinkState, setupDesktopDeepLinkListener, @@ -396,3 +397,36 @@ describe('classifyAuthStoreFailure', () => { expect(classifyAuthStoreFailure(bare)).not.toBe('other'); }); }); + +describe('authStoreFailureUserMessage (issue #3025)', () => { + const CLOUD_KINDS = [ + 'auth_me_timeout', + 'auth_me_unauthorized', + 'auth_me_gateway', + 'network', + 'other', + ]; + + // Local / unset mode always keeps the plain retry message — the failure there + // is a transient embedded-core/backend blip that retrying can clear. + it.each(['local', null] as const)('stays generic for mode=%s', mode => { + for (const kind of CLOUD_KINDS) { + expect(authStoreFailureUserMessage(kind, mode)).toBe('Sign-in failed. Please try again.'); + } + }); + + it('points cloud-mode users at the remote runtime, not a dead-end retry', () => { + for (const kind of CLOUD_KINDS) { + const msg = authStoreFailureUserMessage(kind, 'cloud'); + expect(msg).not.toBe('Sign-in failed. Please try again.'); + expect(msg.toLowerCase()).toContain('remote'); + } + }); + + it('gives kind-specific cloud hints (401 token, gateway/timeout BACKEND_URL)', () => { + expect(authStoreFailureUserMessage('auth_me_unauthorized', 'cloud')).toContain('RPC token'); + expect(authStoreFailureUserMessage('auth_me_gateway', 'cloud')).toContain('BACKEND_URL'); + expect(authStoreFailureUserMessage('auth_me_timeout', 'cloud')).toContain('BACKEND_URL'); + expect(authStoreFailureUserMessage('network', 'cloud')).toContain('online'); + }); +}); diff --git a/app/src/utils/desktopDeepLinkListener.ts b/app/src/utils/desktopDeepLinkListener.ts index 1deef5577..554933b6b 100644 --- a/app/src/utils/desktopDeepLinkListener.ts +++ b/app/src/utils/desktopDeepLinkListener.ts @@ -285,7 +285,7 @@ const handleAuthDeepLink = async (parsed: URL, requireStateNonce = true) => { fingerprint: ['deep-link-auth', 'session-store-failed', kind], }); console.warn('[DeepLink][auth] session store failed — staying on signin (kind=%s)', kind); - failDeepLinkAuthProcessing('Sign-in failed. Please try again.'); + failDeepLinkAuthProcessing(authStoreFailureUserMessage(kind, getStoredCoreMode())); } } }; @@ -321,6 +321,48 @@ export const classifyAuthStoreFailure = (message: string): string => { return 'other'; }; +/** + * Build the user-facing message for an auth-*store* failure (issue #3025). + * + * `auth_store_session` makes the core validate the freshly minted session token + * against the backend `GET /auth/me` before persisting it. In **cloud mode** + * that validation runs on the user's *remote* `openhuman-core`, so the dominant + * failure is the remote runtime being unable to reach/authenticate against the + * backend (misconfigured `BACKEND_URL`, offline, or an older core that predates + * `allowPendingBackendValidation`) — not a problem the desktop can retry away. + * The old blanket "Sign-in failed. Please try again." gave cloud users no path + * forward; point them at the remote runtime instead. Local mode keeps the plain + * retry message (a transient embedded-core/backend blip that retrying can fix). + */ +export const authStoreFailureUserMessage = ( + kind: string, + mode: 'local' | 'cloud' | null +): string => { + if (mode !== 'cloud') { + return 'Sign-in failed. Please try again.'; + } + switch (kind) { + case 'auth_me_unauthorized': + return ( + 'Your remote (cloud) runtime rejected the sign-in. Check the RPC token in ' + + 'Settings and that the runtime points at the correct backend, then try again.' + ); + case 'auth_me_timeout': + case 'auth_me_gateway': + case 'network': + return ( + 'Your remote (cloud) runtime could not reach the backend to finish sign-in. ' + + 'Check that the runtime is online and its BACKEND_URL is configured, then try again.' + ); + default: + return ( + 'Your remote (cloud) runtime could not complete sign-in. Verify it is running ' + + 'and configured correctly (RPC URL/token in Settings, backend connectivity), ' + + 'then try again.' + ); + } +}; + /** * Handle `openhuman://payment/success?session_id=...` deep links. * Fired when a Stripe checkout session completes and the browser redirects diff --git a/gitbooks/features/cloud-deploy.md b/gitbooks/features/cloud-deploy.md index be1a43dd6..f1658bf05 100644 --- a/gitbooks/features/cloud-deploy.md +++ b/gitbooks/features/cloud-deploy.md @@ -399,6 +399,31 @@ Restart the desktop app. The provider chain in `App.tsx` will route all RPC calls to the remote core; nothing else changes. Public `http://` hosts are rejected by the app picker; use HTTPS for any publicly reachable core. +### Troubleshooting: sign-in fails after OAuth on a cloud runtime + +Symptom: OAuth in the browser completes ("close this and return to the app"), +but the desktop then shows a sign-in error — while the **same** account signs in +fine on the local (embedded) runtime (issue #3025). + +Root cause is almost always the **remote** core, not the desktop. `auth_store_session` +makes the core validate the fresh session token against the backend +(`GET /auth/me`) before persisting it; on a cloud runtime that call runs on your +server, so it fails if the remote core can't reach/authenticate the backend: + +- **`BACKEND_URL` unset or wrong.** It is required (see the env table above) and + must be `https://api.tinyhumans.ai` for prod (or the staging URL). A missing + value is the most common cause — one reporter's failure was exactly this. +- **Backend unreachable from the server** (egress firewall, DNS, TLS interception) + → the core sees a gateway/timeout on `/auth/me`. +- **Outdated core.** Older cores predate `allowPendingBackendValidation` and + validate synchronously with no grace; update the server to a current release. +- **Wrong RPC token.** A token mismatch surfaces as HTTP 401 on `/rpc`; re-paste + the token (see "Rotating the bearer token"). + +Check the remote core logs for `Session validation failed (GET /auth/me)` and the +status/reason. The desktop now reports a cloud-specific, actionable message for +these instead of a generic "try again" (issue #3025). + --- ## Named-volume ownership and the Docker entrypoint