// Per-browser persistent client id (design/16 §4.5). Every chat message the Web // client sends carries this id so the agent knows which browser/device the owner // is chatting from — e.g. "web:chrome:a1b2c3". It survives reloads and is shared // by all tabs of the same browser (localStorage), distinguishing multiple browsers // logged into the same account. const CLIENT_ID_KEY = "cyrene.clientId"; /** Short, human-readable browser key derived from the navigator user agent, so the * deviceID reads like "chrome" rather than a raw UA string. */ function browserKey(): string { const ua = navigator.userAgent; if (/edg\//i.test(ua)) return "edge"; if (/firefox\//i.test(ua)) return "firefox"; if (/chrome\//i.test(ua)) return "chrome"; if (/safari\//i.test(ua)) return "safari"; return "browser"; } /** Eight lowercase hex characters of random entropy (one short id token). */ function randomToken(): string { if (typeof crypto !== "undefined" && "getRandomValues" in crypto) { const bytes = new Uint8Array(4); crypto.getRandomValues(bytes); let out = ""; for (const b of bytes) out += b.toString(16).padStart(2, "0"); return out; } return Math.random().toString(16).slice(2, 10); } /** Persistent, per-browser client id: "web::". Stored in * localStorage so every tab of the same browser shares one id and it survives * reloads. Degrades to a fresh id when localStorage is unavailable (privacy mode * / a page that blocked storage): the message is still tagged, just not stable. */ export function getClientId(): string { const existing = localStorage.getItem(CLIENT_ID_KEY); if (existing && existing.startsWith("web:")) return existing; const id = `web:${browserKey()}:${randomToken()}`; try { localStorage.setItem(CLIENT_ID_KEY, id); } catch { // Storage unavailable: fall back to the freshly generated id for this call. } return id; }