From b79eb998857b74b9964b63379497341ac1ea8ab1 Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 18 Mar 2026 20:39:16 -0500 Subject: [PATCH] fix(shared): Handle missing sessionClaims and orgRole in resolveAuthState resolveAuthState had gaps in its condition branches that caused it to return undefined, triggering "Invalid state" errors. This was exposed by #8101 when touch responses came back without last_active_token. Two fixes: - When sessionId/userId exist but sessionClaims is missing (e.g. during client hydration before token fetch), return loading state instead of throwing - When orgId exists but orgRole is missing, fall through to signed-in without org state instead of throwing --- .../src/hooks/__tests__/useAuth.test.tsx | 43 +++++++++++++++++++ packages/shared/src/authorization.ts | 21 ++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/react/src/hooks/__tests__/useAuth.test.tsx b/packages/react/src/hooks/__tests__/useAuth.test.tsx index b395627fc15..40255c87ee0 100644 --- a/packages/react/src/hooks/__tests__/useAuth.test.tsx +++ b/packages/react/src/hooks/__tests__/useAuth.test.tsx @@ -312,6 +312,49 @@ describe('useDerivedAuth', () => { expect(errorThrower.throw).toHaveBeenCalledWith(invalidStateError); }); + it('returns loading state when sessionId and userId are present but sessionClaims is missing', () => { + const authObject = { + sessionId: 'session123', + userId: 'user123', + signOut: vi.fn(), + getToken: vi.fn(), + }; + + const { + result: { current }, + } = renderHook(() => useDerivedAuth(authObject)); + + expect(current.isLoaded).toBe(false); + expect(current.isSignedIn).toBeUndefined(); + expect(current.sessionId).toBeUndefined(); + expect(current.userId).toBeUndefined(); + expect(current.sessionClaims).toBeUndefined(); + }); + + it('returns signed in without org when orgId is present but orgRole is missing', () => { + const authObject = { + sessionId: 'session123', + sessionClaims: stubSessionClaims({ sessionId: 'session123', userId: 'user123', orgId: 'org123' }), + userId: 'user123', + orgId: 'org123', + orgRole: undefined, + signOut: vi.fn(), + getToken: vi.fn(), + }; + + const { + result: { current }, + } = renderHook(() => useDerivedAuth(authObject)); + + expect(current.isLoaded).toBe(true); + expect(current.isSignedIn).toBe(true); + expect(current.sessionId).toBe('session123'); + expect(current.userId).toBe('user123'); + expect(current.orgId).toBeNull(); + expect(current.orgRole).toBeNull(); + expect(current.orgSlug).toBeNull(); + }); + it('uses provided has function if available', () => { const mockHas = vi.fn().mockReturnValue(false); const authObject = { diff --git a/packages/shared/src/authorization.ts b/packages/shared/src/authorization.ts index db2af474f93..f6d9cf76b40 100644 --- a/packages/shared/src/authorization.ts +++ b/packages/shared/src/authorization.ts @@ -342,6 +342,25 @@ const resolveAuthState = ({ } as const; } + // Session exists but claims aren't available yet (e.g. during client hydration + // before a token has been fetched). Treat as loading state. + if (!!sessionId && !!userId && !sessionClaims) { + return { + actor: undefined, + getToken, + has: () => false, + isLoaded: false, + isSignedIn: undefined, + orgId: undefined, + orgRole: undefined, + orgSlug: undefined, + sessionClaims: undefined, + sessionId: undefined, + signOut, + userId: undefined, + } as const; + } + if (!!sessionId && !!sessionClaims && !!userId && !!orgId && !!orgRole) { return { actor: actor || null, @@ -359,7 +378,7 @@ const resolveAuthState = ({ } as const; } - if (!!sessionId && !!sessionClaims && !!userId && !orgId) { + if (!!sessionId && !!sessionClaims && !!userId) { return { actor: actor || null, getToken,