Skip to content

Commit b79eb99

Browse files
committed
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
1 parent ac2f1c1 commit b79eb99

2 files changed

Lines changed: 63 additions & 1 deletion

File tree

packages/react/src/hooks/__tests__/useAuth.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,49 @@ describe('useDerivedAuth', () => {
312312
expect(errorThrower.throw).toHaveBeenCalledWith(invalidStateError);
313313
});
314314

315+
it('returns loading state when sessionId and userId are present but sessionClaims is missing', () => {
316+
const authObject = {
317+
sessionId: 'session123',
318+
userId: 'user123',
319+
signOut: vi.fn(),
320+
getToken: vi.fn(),
321+
};
322+
323+
const {
324+
result: { current },
325+
} = renderHook(() => useDerivedAuth(authObject));
326+
327+
expect(current.isLoaded).toBe(false);
328+
expect(current.isSignedIn).toBeUndefined();
329+
expect(current.sessionId).toBeUndefined();
330+
expect(current.userId).toBeUndefined();
331+
expect(current.sessionClaims).toBeUndefined();
332+
});
333+
334+
it('returns signed in without org when orgId is present but orgRole is missing', () => {
335+
const authObject = {
336+
sessionId: 'session123',
337+
sessionClaims: stubSessionClaims({ sessionId: 'session123', userId: 'user123', orgId: 'org123' }),
338+
userId: 'user123',
339+
orgId: 'org123',
340+
orgRole: undefined,
341+
signOut: vi.fn(),
342+
getToken: vi.fn(),
343+
};
344+
345+
const {
346+
result: { current },
347+
} = renderHook(() => useDerivedAuth(authObject));
348+
349+
expect(current.isLoaded).toBe(true);
350+
expect(current.isSignedIn).toBe(true);
351+
expect(current.sessionId).toBe('session123');
352+
expect(current.userId).toBe('user123');
353+
expect(current.orgId).toBeNull();
354+
expect(current.orgRole).toBeNull();
355+
expect(current.orgSlug).toBeNull();
356+
});
357+
315358
it('uses provided has function if available', () => {
316359
const mockHas = vi.fn().mockReturnValue(false);
317360
const authObject = {

packages/shared/src/authorization.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,25 @@ const resolveAuthState = ({
342342
} as const;
343343
}
344344

345+
// Session exists but claims aren't available yet (e.g. during client hydration
346+
// before a token has been fetched). Treat as loading state.
347+
if (!!sessionId && !!userId && !sessionClaims) {
348+
return {
349+
actor: undefined,
350+
getToken,
351+
has: () => false,
352+
isLoaded: false,
353+
isSignedIn: undefined,
354+
orgId: undefined,
355+
orgRole: undefined,
356+
orgSlug: undefined,
357+
sessionClaims: undefined,
358+
sessionId: undefined,
359+
signOut,
360+
userId: undefined,
361+
} as const;
362+
}
363+
345364
if (!!sessionId && !!sessionClaims && !!userId && !!orgId && !!orgRole) {
346365
return {
347366
actor: actor || null,
@@ -359,7 +378,7 @@ const resolveAuthState = ({
359378
} as const;
360379
}
361380

362-
if (!!sessionId && !!sessionClaims && !!userId && !orgId) {
381+
if (!!sessionId && !!sessionClaims && !!userId) {
363382
return {
364383
actor: actor || null,
365384
getToken,

0 commit comments

Comments
 (0)