// The inviter-name hint (#1436): a person-invite's encrypted payload carries // the inviter's display name (`from`, beside the DM threadKey — the server // can't open either), so the joiner's first paint can label their inviter // instead of showing "Someone" while the instance-key grant is still in // flight. The hint is provisional by construction: it only ever fills an // entry whose real label hasn't decrypted, and it dies the moment one has. // Pure decisions only — join.js/shell.js own the DOM and storage // (Node-tested in test/invitehint.test.mjs). export const INVITER_HINT_KEY = "lchat_inviter_hint"; // A hint outliving a week is a stale artifact of an abandoned join, not a // first paint — and the real label has almost certainly decrypted by then. const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; export function inviterHintStash({ thread, name, now }) { return JSON.stringify({ v: 1, thread: String(thread || ""), name: String(name || "").trim().slice(0, 64), at: now, }); } export function readInviterHint(storedJSON, now) { let h = null; try { h = JSON.parse(storedJSON || "null"); } catch { return null; } if (!h || typeof h.thread !== "string" || typeof h.name !== "string") return null; if (!h.thread || !h.name.trim()) return null; if (typeof h.at !== "number" || now < h.at || now - h.at > MAX_AGE_MS) return null; return { thread: h.thread, name: h.name.trim() }; } // inviterHintTarget: who (if anyone) the hint should provisionally label. // { userId, name } — fill this entry's missing label // { resolved: true } — the real label decrypted; drop the stash // null — nothing to do (thread/member not loaded yet, // or the hint doesn't apply): keep the stash, // a later pass may have more data. // Scoped hard to the hinted DM's other member — a hint must never rename // anyone else, and never a bot (agents carry plaintext labels, never blanks). export function inviterHintTarget(hint, threads, roster, myUserId) { if (!hint) return null; const t = (threads || []).find((x) => x && x.id === hint.thread); if (!t) return null; const otherId = (t.members || []).find((uid) => uid && uid !== myUserId); if (!otherId) return null; const e = (roster || []).find((x) => x && x.user_id === otherId); if (!e || e.is_agent) return null; if (e.label) return { resolved: true }; return { userId: otherId, name: hint.name }; }