feat(uniweb): readable handle instead of endpoint id in messages
Resolve a message sender's endpoint id to a human handle using a new
control-plane directory endpoint.
- ControlPlane.fetchDirectory(): signed GET /api/directory, mapped to
DirectoryEntry { signPub, endpoint, handle, role }. The server's endpoint
matches endpointID(signPub) byte for byte.
- busService keeps an endpoint -> handle Map, loaded once after a session
opens and refreshed after createRoom (where the ACL is already refreshed).
Exposes a pure displayName(endpoint) resolver: handle when known, the
session user's own handle for their messages, short id fallback otherwise.
- Resilience: loadDirectory never throws. A missing endpoint (404 on older
clusters) or a transient error leaves the map empty and the UI falls back to
the short id, so the chat keeps working exactly as before.
- ChatPanel renders displayName(msg.sender) in the message header and derives
the avatar initials from the handle; the raw endpoint stays in a title
tooltip for debugging.
- types: Message.sender comment updated (this is the "phase 2" readable name).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -176,6 +176,29 @@ interface MemberJSON {
|
||||
sign_pub: string; // base64
|
||||
}
|
||||
|
||||
// DirectoryMemberWire is one row of GET /directory: a cluster-wide member with its
|
||||
// human handle and role. sign_pub here is 64-hex (the raw Ed25519 public key), and
|
||||
// endpoint matches endpointID(signPub) byte for byte.
|
||||
interface DirectoryMemberWire {
|
||||
sign_pub: string; // 64-hex
|
||||
endpoint: string; // base64url-nopad, == endpointID(signPub)
|
||||
handle: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface DirectoryResp {
|
||||
members: DirectoryMemberWire[];
|
||||
}
|
||||
|
||||
// DirectoryEntry is the SDK shape of one directory member: the readable handle keyed
|
||||
// by the stable endpoint id, so the UI can show a name instead of the raw id.
|
||||
export interface DirectoryEntry {
|
||||
signPub: string; // 64-hex
|
||||
endpoint: string;
|
||||
handle: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
// MemberRoomWire is one row of GET /members/{endpoint}/rooms.
|
||||
interface MemberRoomWire {
|
||||
room_id: string;
|
||||
@@ -285,6 +308,21 @@ export class ControlPlane {
|
||||
}));
|
||||
}
|
||||
|
||||
// fetchDirectory returns the cluster-wide member directory (GET /api/directory), so
|
||||
// the UI can resolve a message sender's endpoint id to a readable handle. The
|
||||
// request is signed like every other control-plane call. The caller is expected to
|
||||
// tolerate this endpoint being absent on older clusters (404) and fall back to the
|
||||
// short id; this method only maps the wire shape and lets transport errors surface.
|
||||
async fetchDirectory(): Promise<DirectoryEntry[]> {
|
||||
const resp = await this.request<DirectoryResp>("GET", "/directory");
|
||||
return (resp.members ?? []).map((m) => ({
|
||||
signPub: m.sign_pub,
|
||||
endpoint: m.endpoint,
|
||||
handle: m.handle,
|
||||
role: m.role,
|
||||
}));
|
||||
}
|
||||
|
||||
// listMembers returns the room's members keyed by endpoint, so a receiver can find
|
||||
// a sender's signing public key to verify message signatures.
|
||||
async signerKeys(roomID: string): Promise<Map<string, Uint8Array>> {
|
||||
|
||||
Reference in New Issue
Block a user