const { sessionStorage, navigator, document, location, screen, fetch } = globalThis let ENDPOINT = '/t' const SK_UID = '_u' const SK_HASH = '_h' let encKey = null async function importKey(b64) { const raw = Uint8Array.from(globalThis.atob(b64), c => c.codePointAt(0)) return globalThis.crypto.subtle.importKey('raw', raw, 'AES-GCM', false, ['encrypt', 'decrypt']) } async function encrypt(data) { /* c8 ignore next */ /* istanbul ignore next */ if (!encKey) return null const json = new globalThis.TextEncoder().encode(JSON.stringify(data)) const iv = globalThis.crypto.getRandomValues(new Uint8Array(12)) const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv, tagLength: 128 }, encKey, json) const buf = new Uint8Array(encrypted) const combined = new Uint8Array(12 + buf.length) combined.set(iv) combined.set(buf, 12) return globalThis.btoa(String.fromCodePoint(...combined)) } async function decrypt(b64) { /* c8 ignore next */ /* istanbul ignore next */ if (!encKey) return null const raw = Uint8Array.from(globalThis.atob(b64), c => c.codePointAt(0)) const iv = raw.slice(0, 12) const data = raw.slice(12) try { const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv, tagLength: 128 }, encKey, data) return JSON.parse(new globalThis.TextDecoder().decode(decrypted)) } catch { return null } } function clearSession() { sessionStorage.removeItem(SK_UID) sessionStorage.removeItem(SK_HASH) } async function send(data, expectResponse = false) { const d = await encrypt(data) /* c8 ignore next */ /* istanbul ignore next */ if (!d) return null try { if (!expectResponse && navigator.sendBeacon) { navigator.sendBeacon(ENDPOINT, JSON.stringify({ d })) return null } const res = await fetch(ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ d }), keepalive: true, }) if (res.status === 403) { clearSession() return null } if (!res.ok || res.status === 204) return null const json = await res.json() return json.d ? await decrypt(json.d) : null } catch { return null } } async function getOrCreateVisitor() { const uid = sessionStorage.getItem(SK_UID) const hash = sessionStorage.getItem(SK_HASH) if (uid && hash) return { uid, hash } const resp = await send({ sw: screen.width, sh: screen.height, l: navigator.language || null, }, true) if (!resp?.uid || !resp?.h) return null sessionStorage.setItem(SK_UID, resp.uid) sessionStorage.setItem(SK_HASH, resp.h) return { uid: resp.uid, hash: resp.h } } async function trackPageView(visitor) { await send({ uid: visitor.uid, h: visitor.hash, u: location.pathname + location.search, t: document.title, r: document.referrer || null, }) } export async function initAnalytics() { const keyB64 = document.body.dataset.k const ep = document.body.dataset.e if (!keyB64 || !ep) return ENDPOINT = ep try { encKey = await importKey(keyB64) } catch { return } let visitor = await getOrCreateVisitor() if (!visitor) return await trackPageView(visitor) // If trackPageView got 403 (stale session), retry with fresh visitor if (!sessionStorage.getItem(SK_UID)) { visitor = await getOrCreateVisitor() if (!visitor) return await trackPageView(visitor) } const authUserId = document.body.dataset.uid if (authUserId) { await setAuth(Number.parseInt(authUserId, 10)) } } export async function setAuth(userId) { const uid = sessionStorage.getItem(SK_UID) const hash = sessionStorage.getItem(SK_HASH) if (!uid || !hash || !encKey) return await send({ uid, h: hash, setUser: userId }) }