fix(socket): use core auth user id for socket state scoping (#1984)

This commit is contained in:
Ruifeng Xue
2026-05-17 01:37:30 -07:00
committed by GitHub
parent d412aaa72f
commit 3aa8477e1a
3 changed files with 62 additions and 24 deletions
@@ -27,7 +27,9 @@ vi.mock('../../store/channelConnectionsSlice', () => ({
upsertChannelConnection: vi.fn((x: unknown) => x),
}));
vi.mock('../../lib/coreState/store', () => ({
getCoreStateSnapshot: vi.fn(() => ({ snapshot: { sessionToken: null } })),
getCoreStateSnapshot: vi.fn(() => ({
snapshot: { auth: { userId: 'core-user-id' }, sessionToken: null },
})),
}));
class MockMCPTransport {}
vi.mock('../../lib/mcp', () => ({ SocketIOMCPTransportImpl: MockMCPTransport }));
@@ -47,7 +47,9 @@ vi.mock('../../store/connectivitySlice', () => ({
// Mock coreState
vi.mock('../../lib/coreState/store', () => ({
getCoreStateSnapshot: vi.fn(() => ({ snapshot: { sessionToken: null } })),
getCoreStateSnapshot: vi.fn(() => ({
snapshot: { auth: { userId: 'core-user-id' }, sessionToken: null },
})),
}));
// Mock MCP as a class so `new SocketIOMCPTransportImpl(...)` works at runtime.
@@ -98,6 +100,61 @@ describe('socketService — resolveCoreSocketBaseUrl uses getCoreRpcUrl', () =>
await pollUntil(() => expect(hoisted.getCoreRpcUrlMock).toHaveBeenCalled());
});
it('scopes socket state from core auth userId instead of decoding the JWT payload', async () => {
const { getCoreStateSnapshot } = await import('../../lib/coreState/store');
const { setStatusForUser } = await import('../../store/socketSlice');
const setStatusForUserMock = vi.mocked(setStatusForUser);
setStatusForUserMock.mockClear();
vi.mocked(getCoreStateSnapshot).mockReturnValue({
snapshot: {
auth: { userId: 'core-user-id' },
sessionToken: 'header.eyJ1c2VySWQiOiJqd3QtdXNlci1pZCJ9.signature',
},
} as ReturnType<typeof getCoreStateSnapshot>);
hoisted.getCoreRpcUrlMock.mockResolvedValue('http://127.0.0.1:7788/rpc');
const { socketService } = await import('../socketService');
socketService.disconnect();
socketService.connect('header.eyJ1c2VySWQiOiJqd3QtdXNlci1pZCJ9.signature');
await pollUntil(() =>
expect(setStatusForUserMock).toHaveBeenCalledWith({
userId: 'core-user-id',
status: 'connecting',
})
);
expect(setStatusForUserMock).not.toHaveBeenCalledWith({
userId: 'jwt-user-id',
status: 'connecting',
});
});
it('falls back to pending when the core auth snapshot is not available yet', async () => {
const { getCoreStateSnapshot } = await import('../../lib/coreState/store');
const { setStatusForUser } = await import('../../store/socketSlice');
const setStatusForUserMock = vi.mocked(setStatusForUser);
vi.mocked(getCoreStateSnapshot).mockReturnValue({
snapshot: { sessionToken: 'mock-token' },
} as ReturnType<typeof getCoreStateSnapshot>);
hoisted.getCoreRpcUrlMock.mockResolvedValue('http://127.0.0.1:7788/rpc');
const { socketService } = await import('../socketService');
socketService.disconnect();
setStatusForUserMock.mockClear();
socketService.connect('mock-token-with-missing-auth');
await pollUntil(() =>
expect(setStatusForUserMock).toHaveBeenCalledWith({
userId: '__pending__',
status: 'connecting',
})
);
});
it('strips /rpc suffix from the resolved RPC URL to derive the socket base', async () => {
const { io } = await import('socket.io-client');
const ioMock = vi.mocked(io);
+1 -22
View File
@@ -39,12 +39,6 @@ async function resolveCoreSocketBaseUrl(): Promise<string> {
return coreSocketBaseFromRpcUrl(rpcUrl);
}
interface JwtPayload {
tgUserId?: string;
userId?: string;
sub?: string;
}
interface ChannelConnectionUpdatedEvent {
channel: ChannelType;
authMode: ChannelAuthMode;
@@ -99,22 +93,7 @@ function normalizeChannelConnectionUpdatePayload(
}
function getSocketUserId(): string {
const token = getCoreStateSnapshot().snapshot.sessionToken;
if (!token) return '__pending__';
try {
const parts = token.split('.');
if (parts.length !== 3) return '__pending__';
const payloadBase64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
const payloadJson = atob(payloadBase64);
const payload = JSON.parse(payloadJson) as JwtPayload;
const id = payload.tgUserId || payload.userId || payload.sub;
return id || '__pending__';
} catch {
return '__pending__';
}
return getCoreStateSnapshot().snapshot?.auth?.userId ?? '__pending__';
}
class SocketService {