fix: federation DM identity resolution — cross-instance isSelf() failure
Add a cross-instance self-ID registry to identity.ts so isSelf() can recognize the current user's Snowflake IDs from all connected instances. Previously, federated DMs showed the user themselves as the other party because remote-instance IDs didn't match the home user ID. - Register user IDs from every WS ready event (home + remote) - Clear the registry on session reset (login/logout/register/delete) - Fix isSelf() username comparison to parse both sides as federated - Replace naive ID check in MessageList WelcomeHeader with isSelf()
This commit is contained in:
@@ -7,6 +7,7 @@ import { useSocialStore } from '../../stores/socialStore';
|
|||||||
import { Avatar } from '../ui/Avatar';
|
import { Avatar } from '../ui/Avatar';
|
||||||
import { LoadingSpinner } from '../ui/LoadingSpinner';
|
import { LoadingSpinner } from '../ui/LoadingSpinner';
|
||||||
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
|
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
|
||||||
|
import { isSelf } from '../../utils/identity';
|
||||||
import type { MessageWithUser } from '@backspace/shared';
|
import type { MessageWithUser } from '@backspace/shared';
|
||||||
|
|
||||||
const EMPTY_MESSAGES: MessageWithUser[] = [];
|
const EMPTY_MESSAGES: MessageWithUser[] = [];
|
||||||
@@ -247,7 +248,7 @@ function WelcomeHeader({ channelId }: { channelId: string }) {
|
|||||||
|
|
||||||
if (isDm) {
|
if (isDm) {
|
||||||
const dm = dmChannels.find(d => d.id === channelId);
|
const dm = dmChannels.find(d => d.id === channelId);
|
||||||
const otherUser = dm?.members.find(m => m.id !== authUser?.id);
|
const otherUser = dm?.members.find(m => !isSelf(m, authUser));
|
||||||
const displayName = otherUser?.displayName ?? otherUser?.username ?? 'Unknown';
|
const displayName = otherUser?.displayName ?? otherUser?.username ?? 'Unknown';
|
||||||
const username = otherUser?.username ?? 'unknown';
|
const username = otherUser?.username ?? 'unknown';
|
||||||
const isFriend = otherUser ? friends.some(f => f.id === otherUser.id) : false;
|
const isFriend = otherUser ? friends.some(f => f.id === otherUser.id) : false;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useSettingsStore } from '../stores/settingsStore';
|
|||||||
import type { ServerEvent, ClientEvent, ActiveCallInfo } from '@backspace/shared';
|
import type { ServerEvent, ClientEvent, ActiveCallInfo } from '@backspace/shared';
|
||||||
import { resolveAssetUrl, normalizeUserAssets, normalizeMessageAssets } from '../utils/assetUrls';
|
import { resolveAssetUrl, normalizeUserAssets, normalizeMessageAssets } from '../utils/assetUrls';
|
||||||
import { broadcastVoiceStatus, broadcastDeafenViaLiveKit } from '../utils/voice';
|
import { broadcastVoiceStatus, broadcastDeafenViaLiveKit } from '../utils/voice';
|
||||||
|
import { registerSelfId } from '../utils/identity';
|
||||||
|
|
||||||
// ─── Connection state ─────────────────────────────────────────────────────────
|
// ─── Connection state ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -88,6 +89,9 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
|||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'ready':
|
case 'ready':
|
||||||
|
// Register this user's ID for cross-instance self-identification
|
||||||
|
registerSelfId(event.user.id);
|
||||||
|
|
||||||
if (isHome) {
|
if (isHome) {
|
||||||
setUser(event.user);
|
setUser(event.user);
|
||||||
useSettingsStore.getState().setIsAdmin(event.user.isAdmin ?? false);
|
useSettingsStore.getState().setIsAdmin(event.user.isAdmin ?? false);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useVoiceStore } from './voiceStore';
|
|||||||
import { useInstanceStore } from './instanceStore';
|
import { useInstanceStore } from './instanceStore';
|
||||||
import { syncProfileUpdateToRemotes } from '../utils/profileSync';
|
import { syncProfileUpdateToRemotes } from '../utils/profileSync';
|
||||||
import { changePasswordOnRemotes, deleteAccountOnRemotes, type FederationOpResult } from '../utils/federationOps';
|
import { changePasswordOnRemotes, deleteAccountOnRemotes, type FederationOpResult } from '../utils/federationOps';
|
||||||
|
import { clearSelfIds } from '../utils/identity';
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
token: string | null;
|
token: string | null;
|
||||||
@@ -27,6 +28,7 @@ interface AuthState {
|
|||||||
|
|
||||||
/** Reset all user-scoped stores to prevent data leaking between sessions */
|
/** Reset all user-scoped stores to prevent data leaking between sessions */
|
||||||
function resetUserStores() {
|
function resetUserStores() {
|
||||||
|
clearSelfIds();
|
||||||
useChatStore.getState().clearAllMessages();
|
useChatStore.getState().clearAllMessages();
|
||||||
useSpaceStore.getState().populateFromReady('', [], [], []);
|
useSpaceStore.getState().populateFromReady('', [], [], []);
|
||||||
useSocialStore.getState().reset();
|
useSocialStore.getState().reset();
|
||||||
|
|||||||
@@ -11,6 +11,20 @@ export function parseFederatedUsername(username: string): { baseName: string; do
|
|||||||
return { baseName: username.slice(0, atIndex), domain: username.slice(atIndex + 1) };
|
return { baseName: username.slice(0, atIndex), domain: username.slice(atIndex + 1) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Cross-instance self-ID registry ─────────────────────────────────────────
|
||||||
|
// Tracks all Snowflake IDs that belong to the current user across connected
|
||||||
|
// instances (home + remotes). Populated from WS `ready` events.
|
||||||
|
|
||||||
|
const _knownSelfIds = new Set<string>();
|
||||||
|
|
||||||
|
export function registerSelfId(id: string): void {
|
||||||
|
_knownSelfIds.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearSelfIds(): void {
|
||||||
|
_knownSelfIds.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stateless check: is `user` a replicated alias of `homeUser`?
|
* Stateless check: is `user` a replicated alias of `homeUser`?
|
||||||
* Uses the immutable (username, homeInstance) composite key —
|
* Uses the immutable (username, homeInstance) composite key —
|
||||||
@@ -23,12 +37,15 @@ export function isSelf(
|
|||||||
if (!homeUser) return false;
|
if (!homeUser) return false;
|
||||||
// Same instance, same ID — trivial case
|
// Same instance, same ID — trivial case
|
||||||
if (user.id === homeUser.id) return true;
|
if (user.id === homeUser.id) return true;
|
||||||
|
// Cross-instance: check all known user IDs from connected instances
|
||||||
|
if (_knownSelfIds.has(user.id)) return true;
|
||||||
// Replicated user: homeInstance matches our origin
|
// Replicated user: homeInstance matches our origin
|
||||||
if (!user.homeInstance) return false;
|
if (!user.homeInstance) return false;
|
||||||
if (user.homeInstance !== window.location.host) return false;
|
if (user.homeInstance !== window.location.host) return false;
|
||||||
// Username: "youruser" or "youruser@nova.ddns.net" → base must match
|
// Username: "youruser" or "youruser@nova.ddns.net" → base must match
|
||||||
const { baseName } = parseFederatedUsername(user.username);
|
const { baseName } = parseFederatedUsername(user.username);
|
||||||
return baseName === homeUser.username;
|
const { baseName: homeBase } = parseFederatedUsername(homeUser.username);
|
||||||
|
return baseName === homeBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user