Developer Docs

User Identity Resolution

The Mallnline frontend stores user relationships as opaque UUIDs โ€” team members, collaborators, blog authors, and Murchase participants are all referenced by their internal userId. Displaying these raw identifiers to end users is a security and UX violation.

The Profile Resolver is a shared utility that batch-resolves user IDs into human-readable profiles (v|handle, displayName, or anonymous fallbacks). It is consumed by every component that displays user attribution, most notably the ActorBadge component.

The Problem

Many backend relationships reference users by their auth-service UUID:

// What the backend returns
{ userId: "a3f8c912-7b4d-4e1a-9f2c-3d5e7a8b1c0d", role: "LEAD" }

// What the user sees (BEFORE fix)
"a3f8c912โ€ฆ"

// What the user sees (AFTER fix)
"@meekdenzo" or "Meek Denzo"

The Handle System provides the sigil taxonomy (v|meekdenzo, @meekdenzo), but until profile data is fetched from the nodes subgraph, the frontend only has raw UUIDs. The Profile Resolver bridges this gap.

Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   TeamCard.svelteโ”‚     โ”‚ CollaboratorCard โ”‚     โ”‚ BlogAnalytics   โ”‚
โ”‚   TeamsPanel     โ”‚     โ”‚ CollabPanel      โ”‚     โ”‚                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚                       โ”‚                       โ”‚
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                    โ–ผ
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚  profileResolver.ts โ”‚  โ† Shared utility
         โ”‚  resolveUserProfilesโ”‚
         โ”‚  formatUserDisplay  โ”‚
         โ”‚  getAvatarInitials  โ”‚
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ–ผ
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚  GET_USER_BY_ID     โ”‚  โ† GraphQL query (nodes subgraph)
         โ”‚  user(id: $id) {    โ”‚
         โ”‚    id, displayName, โ”‚
         โ”‚    handle, avatarUrlโ”‚
         โ”‚  }                  โ”‚
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

API Reference

`resolveUserProfiles(userIds, existingCache?)`

Batch-resolves an array of user UUIDs into profile info. Deduplicates before querying and merges results into the provided cache.

import { resolveUserProfiles, type ProfileMap } from '$lib/utils/profileResolver';

const profiles: ProfileMap = await resolveUserProfiles(
  ['uuid-1', 'uuid-2', 'uuid-3'],
  existingCache  // Optional โ€” avoids re-fetching known profiles
);

Parameters:

Param Type Description
userIds string[] Array of user UUIDs to resolve
existingCache ProfileMap Optional existing cache to avoid re-fetching

Returns: Promise<ProfileMap> โ€” merged map of userId โ†’ UserProfileInfo

Error handling: Individual resolution failures are silently absorbed โ€” the profile entry receives a stub { id } so formatUserDisplay falls back to truncated UUID. This prevents a single missing profile from breaking the entire batch.

`formatUserDisplay(profiles, userId)`

Returns the best available display string for a user, following the priority chain:

v|handle  โ†’  displayName  โ†’  'Mallnline User'
import { formatUserDisplay } from '$lib/utils/profileResolver';

formatUserDisplay(profiles, 'uuid-1');
// โ†’ "v|meekdenzo"      (if handle exists)
// โ†’ "Meek Denzo"       (if only displayName exists)
// โ†’ "Mallnline User"   (anonymous fallback โ€” never exposes raw IDs)

`getAvatarInitials(profiles, userId)`

Returns 1โ€“2 uppercase initials from the resolved displayName, or the first character of the userId as fallback.

import { getAvatarInitials } from '$lib/utils/profileResolver';

getAvatarInitials(profiles, 'uuid-1');
// โ†’ "MD"   (from "Meek Denzo")
// โ†’ "M"    (from "Madonna")
// โ†’ "MU"   (anonymous fallback โ€” never derives from userId)

Integration Pattern

Components that display user attribution follow a consistent pattern:

1. Parent resolves profiles after data load

// In the parent panel/page (e.g., CollaboratorsPanel.svelte)
import { resolveUserProfiles, type ProfileMap } from '$lib/utils/profileResolver';

let profiles: ProfileMap = $state({});

async function fetchData() {
  const data = await client.request(QUERY);
  const userIds = data.items.map(item => item.userId);
  profiles = await resolveUserProfiles(userIds, profiles);
}

2. Child receives profiles as prop

// In the child card (e.g., CollaboratorCard.svelte)
import { formatUserDisplay, getAvatarInitials, type ProfileMap } from '$lib/utils/profileResolver';

let { memberProfiles = {} }: { memberProfiles?: ProfileMap } = $props();

3. Template renders resolved names

<div class="avatar">{getAvatarInitials(memberProfiles, collaborator.userId)}</div>
<strong>{formatUserDisplay(memberProfiles, collaborator.userId)}</strong>

Components Using Profile Resolution

Component Source of User IDs Resolution Strategy
TeamCard TeamMembership.userId Profiles passed from org manage page via TeamsPanel
CollaboratorCard Collaborator.userId Profiles resolved in CollaboratorsPanel on load
BlogAnalytics TopBlogAuthor.authorId Profiles resolved inline after analytics fetch
Org Manage Page org.members[].userId Resolves all member profiles on org load (existing resolveMemberProfiles)
AuditLogPanel Audit entry actor IDs Resolves via GET_USER_BY_ID per unique actor (existing)
UserListingPanel AdminUserNode.userId Admin-facing โ€” intentionally shows raw UUID in detail drawer as debug field

Relationship to Existing Resolution

The org manage page (/orgs/[slug]/manage) already had a local resolveMemberProfiles() function and getMemberDisplayName() helper. The new profileResolver.ts utility is the extracted, centralized version of this pattern. The manage page's existing implementation remains in place (it predated the utility), but all new components should import from profileResolver.ts.

Migration Path

Existing resolution code in the org manage page can optionally be refactored to use profileResolver.ts:

- import { GET_USER_BY_ID, type UserProfileInfo } from '$lib/queries/auth';
+ import { resolveUserProfiles, formatUserDisplay, type ProfileMap } from '$lib/utils/profileResolver';

- async function resolveMemberProfiles(members) { ... }
- function getMemberDisplayName(userId) { ... }
+ // Use resolveUserProfiles() and formatUserDisplay() directly

This is not urgent โ€” the existing code works correctly and the utility is API-compatible.

Security Audit Coverage

The Profile Resolver was created as part of the Raw User ID Exposure Audit (P0 Security). The audit covered 10 frontend sub-areas:

Area Result
uChat user search/DM โœ… Already clean
Community content (reviews, comments) โœ… Already clean
Workroom/issue assignment โœ… Already clean
Team membership lists โœ… Fixed via profileResolver.ts
Admin user listing โœ… Acceptable (admin field)
Murchase/order history โœ… Already clean
Blog post authorship โœ… Fixed via profileResolver.ts
URL parameter leakage โœ… Already clean
API response sanitization โš ๏ธ Deferred (backend scope)
Error messages โœ… Already clean

File Reference

File Purpose
src/lib/utils/profileResolver.ts Shared batch resolver utility
src/lib/utils/__tests__/profileResolver.test.ts 10 unit tests
src/lib/queries/auth.ts GET_USER_BY_ID query + UserProfileInfo type
src/lib/components/teams/TeamCard.svelte Team member display (consumes profiles)
src/lib/components/teams/TeamsPanel.svelte Teams container (passes profiles)
src/lib/components/teams/CollaboratorCard.svelte Collaborator display (consumes profiles)
src/lib/components/teams/CollaboratorsPanel.svelte Collaborator container (resolves profiles)
src/lib/components/admin/BlogAnalytics.svelte Blog author leaderboard (resolves profiles)