'use client'; /** * Owner token for watchlists and alerts (no account, spec §55): a random string generated once in the browser * (`crypto.randomUUID()` twice → 72 chars) and kept in localStorage. Sent as `X-CA-Owner-Token`; the API stores a hash. * Losing the browser storage loses the watchlist — the /watchlist page says so and offers export/import of the token. */ import { useEffect, useState } from 'react'; export const OWNER_KEY = 'ca-owner-token'; const EVENT = 'ca-owner-change'; export function readOwnerToken(): string | null { if (typeof window === 'undefined') return null; try { const v = window.localStorage.getItem(OWNER_KEY); return v && v.length >= 24 ? v : null; } catch { return null; } } export function ensureOwnerToken(): string { const cur = readOwnerToken(); if (cur) return cur; const t = `${crypto.randomUUID()}${crypto.randomUUID()}`.replace(/-/g, ''); try { window.localStorage.setItem(OWNER_KEY, t); window.dispatchEvent(new CustomEvent(EVENT)); } catch { /* storage disabled: token lives for this page only */ } return t; } export function setOwnerToken(t: string): boolean { if (t.trim().length < 24) return false; try { window.localStorage.setItem(OWNER_KEY, t.trim()); window.dispatchEvent(new CustomEvent(EVENT)); return true; } catch { return false; } } /** Token after mount (null during SSR / first paint). `create` generates one when absent. */ export function useOwnerToken(create = false): string | null { const [token, setToken] = useState(null); useEffect(() => { const read = () => setToken(create ? ensureOwnerToken() : readOwnerToken()); read(); window.addEventListener(EVENT, read); window.addEventListener('storage', read); return () => { window.removeEventListener(EVENT, read); window.removeEventListener('storage', read); }; }, [create]); return token; } /** Local mirror of watched slugs so watch buttons render instantly and work when the API is briefly unavailable. */ export const WATCHED_KEY = 'ca-watched'; const WATCHED_EVENT = 'ca-watched-change'; export function readWatched(): string[] { if (typeof window === 'undefined') return []; try { const arr = JSON.parse(window.localStorage.getItem(WATCHED_KEY) ?? '[]') as unknown; return Array.isArray(arr) ? arr.filter((x): x is string => typeof x === 'string') : []; } catch { return []; } } export function writeWatched(slugs: string[]) { try { window.localStorage.setItem(WATCHED_KEY, JSON.stringify([...new Set(slugs)])); } catch { /* ignore */ } window.dispatchEvent(new CustomEvent(WATCHED_EVENT)); } export function useWatched(): string[] { const [w, setW] = useState([]); useEffect(() => { const read = () => setW(readWatched()); read(); window.addEventListener(WATCHED_EVENT, read); window.addEventListener('storage', read); return () => { window.removeEventListener(WATCHED_EVENT, read); window.removeEventListener('storage', read); }; }, []); return w; }