eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1// The inviter-name hint (#1436): a person-invite's encrypted payload carries
2// the inviter's display name (`from`, beside the DM threadKey — the server
3// can't open either), so the joiner's first paint can label their inviter
4// instead of showing "Someone" while the instance-key grant is still in
5// flight. The hint is provisional by construction: it only ever fills an
6// entry whose real label hasn't decrypted, and it dies the moment one has.
7// Pure decisions only — join.js/shell.js own the DOM and storage
8// (Node-tested in test/invitehint.test.mjs).
9
10export const INVITER_HINT_KEY = "lchat_inviter_hint";
11
12// A hint outliving a week is a stale artifact of an abandoned join, not a
13// first paint — and the real label has almost certainly decrypted by then.
14const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
15
16export function inviterHintStash({ thread, name, now }) {
17 return JSON.stringify({
18 v: 1,
19 thread: String(thread || ""),
20 name: String(name || "").trim().slice(0, 64),
21 at: now,
22 });
23}
24
25export function readInviterHint(storedJSON, now) {
26 let h = null;
27 try { h = JSON.parse(storedJSON || "null"); } catch { return null; }
28 if (!h || typeof h.thread !== "string" || typeof h.name !== "string") return null;
29 if (!h.thread || !h.name.trim()) return null;
30 if (typeof h.at !== "number" || now < h.at || now - h.at > MAX_AGE_MS) return null;
31 return { thread: h.thread, name: h.name.trim() };
32}
33
34// inviterHintTarget: who (if anyone) the hint should provisionally label.
35// { userId, name } — fill this entry's missing label
36// { resolved: true } — the real label decrypted; drop the stash
37// null — nothing to do (thread/member not loaded yet,
38// or the hint doesn't apply): keep the stash,
39// a later pass may have more data.
40// Scoped hard to the hinted DM's other member — a hint must never rename
41// anyone else, and never a bot (agents carry plaintext labels, never blanks).
42export function inviterHintTarget(hint, threads, roster, myUserId) {
43 if (!hint) return null;
44 const t = (threads || []).find((x) => x && x.id === hint.thread);
45 if (!t) return null;
46 const otherId = (t.members || []).find((uid) => uid && uid !== myUserId);
47 if (!otherId) return null;
48 const e = (roster || []).find((x) => x && x.user_id === otherId);
49 if (!e || e.is_agent) return null;
50 if (e.label) return { resolved: true };
51 return { userId: otherId, name: hint.name };
52}
53