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:
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user