feat: stateless federated identity resolver + federation UX improvements

Add identity.ts with isSelf() and resolveDisplayIdentity() — pure
stateless functions that detect replicated-self using the immutable
(username, homeInstance) composite key. No store lookups, no data
mutation. Fixes wrong avatar gradient and missing edit/delete on
own messages in remote channels.

Also includes: optimistic message dedup fix for cross-instance
messages (content-only matching), federation toast notifications,
Username component with @domain display, invite parser, and
deploy script simplification.
This commit is contained in:
Jannis Braun
2026-03-04 03:37:49 +01:00
parent 64fd8edfc9
commit dca9c4dc83
19 changed files with 713 additions and 110 deletions
+32
View File
@@ -0,0 +1,32 @@
import type { User } from '@backspace/shared';
/**
* Stateless check: is `user` a replicated alias of `homeUser`?
* Uses the immutable (username, homeInstance) composite key —
* no store lookups, no snowflake ID mapping.
*/
export function isSelf(
user: { id: string; username: string; homeInstance?: string | null },
homeUser: { id: string; username: string } | null,
): boolean {
if (!homeUser) return false;
// Same instance, same ID — trivial case
if (user.id === homeUser.id) return true;
// Replicated user: homeInstance matches our origin
if (!user.homeInstance) return false;
if (user.homeInstance !== window.location.host) return false;
// Username: "youruser" or "youruser@nova.ddns.net" → base must match
const baseUsername = user.username.split('@')[0];
return baseUsername === homeUser.username;
}
/**
* If `user` is a replicated alias of `homeUser`, return `homeUser`
* for display purposes (avatar gradient, display name). Otherwise
* return the original user unchanged. Data is never mutated.
*/
export function resolveDisplayIdentity(user: User, homeUser: User | null): User {
if (!homeUser) return user;
if (isSelf(user, homeUser)) return homeUser;
return user;
}
+64
View File
@@ -0,0 +1,64 @@
/**
* Parse invite input into a code and optional remote origin.
*
* Supported formats:
* - Bare code: "a3f1b2c4"
* - Full URL: "https://remote.com/join/a3f1b2c4"
* - Qualified code: "a3f1b2c4@remote.com"
*/
export function parseInviteInput(input: string): { code: string; origin?: string } {
const trimmed = input.trim();
if (!trimmed) throw new Error('Invite code is required');
// Full URL: starts with http:// or https://
if (/^https?:\/\//i.test(trimmed)) {
let parsed: URL;
try {
parsed = new URL(trimmed);
} catch {
throw new Error('Invalid invite link');
}
// Extract code from /join/{code} path
const match = parsed.pathname.match(/^\/join\/([^/]+)$/);
if (!match) {
throw new Error('Invalid invite link — expected format: https://instance/join/CODE');
}
const code = match[1]!;
// If the URL points at our own instance, treat as a bare code
if (parsed.origin === window.location.origin) {
return { code };
}
return { code, origin: parsed.origin };
}
// Qualified code: CODE@domain (contains @ but no spaces, no protocol)
if (trimmed.includes('@') && !trimmed.includes(' ')) {
const atIndex = trimmed.indexOf('@');
const code = trimmed.slice(0, atIndex);
const domain = trimmed.slice(atIndex + 1);
if (!code || !domain) {
throw new Error('Invalid invite format — expected: CODE@domain');
}
const origin = `https://${domain}`;
// If it resolves to our own instance, treat as bare code
try {
if (new URL(origin).origin === window.location.origin) {
return { code };
}
} catch {
throw new Error('Invalid domain in invite');
}
return { code, origin };
}
// Bare code
return { code: trimmed };
}