/** * 控制台统一请求与登录会话入口。 * 页面通过 useApi 访问 /console/v1,通过 useAuth 共享登录状态;本模块只在客户端使用。 * 后端业务成功必须同时满足 HTTP 成功和响应信封 status === 200,不能仅检查 HTTP 状态。 */ import CryptoJS from "crypto-js"; import { defineStore } from "pinia"; import { navigateTo, useRuntimeConfig } from "#app"; import { computed, ref } from "vue"; // 将供应商/模块内部错误转成面向会员的提示,避免在 UI 中暴露上游实现细节。 var ce = [ [/产品类型错误.{0,30}安全组[^\n]*/g, `该商品未接入安全组`], [/商品未关联.{0,20}模块接口/g, `该商品暂不可用`], [/未关联.{0,12}模块接口/g, `该商品暂不可用`], [/尚未接入.{0,12}模块/g, `该商品暂不可用`], ]; var le = [ [/快快云-弹性云|弹性云/g, ``], [/快快云|快快/g, ``], [/魔方云|魔方财务|智简魔方|智简/g, ``], [/美得云|美德云|meidecloud/gi, ``], [/dwvps[a-z0-9_]*/gi, ``], [/miloce[a-z0-9_]*/gi, ``], [/jinghanyun[a-z0-9_]*/gi, ``], [/kkidc[a-z0-9_]*/gi, ``], [/kky[a-z0-9_]*/gi, ``], [/idcsmart[a-z0-9_]*/gi, ``], [/mf[_-]?cloud[a-z0-9_]*/gi, ``], [/mf[_-]?dcim[a-z0-9_]*/gi, ``], [/common[_-]?cloud[a-z0-9_]*/gi, ``], [/pve[_-]?cloud[a-z0-9_]*/gi, ``], ]; var ue = /模块接口|自定义配置|模块未关联|模块标识|上游|第三方云|[a-z]{2,}[_-][a-z0-9_]+|\bDCIM\b|whmcs|reserver|https?:\/\//i; /** 非字符串输入视为空;替换后仍包含内部标识的消息统一降级,不将原始异常直接展示。 */ function W(e) { let t = typeof e == `string` ? e : ``; if (!t) return ``; let n = t; for (let [e, t] of ce) n = n.replace(e, t); for (let [e, t] of le) n = n.replace(e, t); return ( (n = n .replace(/\s{2,}/g, ` `) .replace(/[,,、]{2,}/g, `,`) .replace(/^[\s,,、::;;-]+|[\s,,、::;;-]+$/g, ``) .trim()), n.length < 2 || ue.test(n) ? `操作失败,请稍后重试或联系客服` : n ); } // 操作日志中的熔断事件单独翻译;其余日志保留原文,由调用方决定展示方式。 function de(e) { let t = typeof e == `string` ? e : ``; return /^\s*上游熔断解除[::]/u.test(t) ? `服务连接已恢复正常` : /^\s*上游熔断[::]/u.test(t) ? `服务连接暂时异常,请稍后重试` : t; } // 列表缓存最多 32 项,每项不超过 200 条、存活 60 秒;身份切换或写操作会使其失效。 var fe = 32; var pe = 200; var me = 6e4; var G = new Map(); var he = Symbol(`uninitialized`); var ge = 0; // 递增代次,阻止清空之前发出的旧请求在完成后重新写回缓存。 function _e() { (ge++, G.clear()); } function ve() { return ge; } /** 按身份/命名空间、接口及参数生成稳定键;对象键排序避免同义参数重复缓存。 */ function ye(e, t, n = ``) { return JSON.stringify([e, n, t], (e, t) => t && typeof t == `object` && !Array.isArray(t) ? Object.fromEntries( Object.keys(t) .sort() .map((e) => [e, t[e]]), ) : t, ); } // 缓存只保存可 JSON 序列化的数据;读写均复制,避免页面修改共享对象。 function be(e) { return JSON.parse(JSON.stringify(e)); } /** e 为当前身份,t 为缓存键;切换身份清空缓存,命中时移动到 Map 尾部维护 LRU。 */ function xe(e, t, n = Date.now()) { he !== e && (_e(), (he = e)); for (let [e, t] of G) t.expires <= n && G.delete(e); let r = G.get(t); if (r) return (G.delete(t), G.set(t, r), be(r.data)); } /** 仅接受身份和请求开始时代次均未变化的列表结果;过大的结果或序列化失败不缓存。 */ function Se(e, t, n, r, i = Date.now()) { if (e === he && r === ge) { if (!Array.isArray(n.list) || n.list.length > pe) { G.delete(t); return; } try { let e = be(n); (G.delete(t), G.size >= fe && G.delete(G.keys().next().value), G.set(t, { data: e, expires: i + me, })); } catch { G.delete(t); } } } var Ce = 2147483647; // 购买协议的整数范围与后端有符号 32 位整数一致;位置允许 0,商品和数量必须大于 0。 function K(e, t) { let n = typeof e == `number` ? e : typeof e == `string` && /^\d+$/.test(e) ? Number(e) : NaN; if (!Number.isSafeInteger(n) || n < t || n > Ce) throw Error(`购买信息无效,请重新确认后重试`); return n; } function we(e) { if (!e || typeof e != `object` || Array.isArray(e)) throw Error(`购买信息无效,请重新确认后重试`); return e; } function Te(e) { let t = we(e); return { product_id: K(t.product_id, 1), qty: K(t.qty, 1), }; } /** * 从实际写请求提取限购检查所需字段;未匹配的接口返回 null,不进入购买预检。 * 购物车位置去重,批量添加最多 1000 项;此处校验不替代后端的最终授权。 */ function Ee(e, t, n) { if (e === `POST` && t === `/product/settle`) return { mode: `single`, ...Te(n), }; if (e === `POST` && t === `/cart`) { let e = we(n); if (e.products !== void 0) { if ( !Array.isArray(e.products) || !e.products.length || e.products.length > 1e3 ) throw Error(`购买信息无效,请重新确认后重试`); return { mode: `cart_add`, products: e.products.map(Te), }; } return { mode: `cart_add`, ...Te(e), }; } if (e === `POST` && t === `/cart/settle`) { let e = we(n); if (!Array.isArray(e.positions) || !e.positions.length) throw Error(`请选择要结算的商品`); return { mode: `cart_settle`, positions: [...new Set(e.positions.map((e) => K(e, 0)))], }; } let r = e === `PUT` ? /^\/cart\/(\d+)(\/qty)?$/.exec(t) : null; if (r) { let e = we(n); return { mode: `cart_update`, position: K(r[1], 0), qty: K(e.qty, 1), ...(r[2] ? {} : { product_id: K(e.product_id, 1), }), }; } return null; } // 预检必须明确允许/拒绝,拒绝时必须给出非空原因;缺失字段不能被当成放行。 function De(e) { if (!e || typeof e != `object` || Array.isArray(e)) return !1; let t = e; return ( typeof t.allowed == `boolean` && typeof t.message == `string` && (t.allowed || !!t.message.trim()) ); } /** 统一传递 HTTP/业务状态和可供安全验证等流程读取的数据,不丢失错误分类。 */ class ApiError extends Error { status; data; constructor(status, message, data) { super(message); this.status = status; this.data = data; this.name = "ApiError"; } } const DEFAULT_REQUEST_TIMEOUT_MS = 30_000; const FILE_TRANSFER_TIMEOUT_MS = 120_000; function requestTimeoutError() { return new ApiError(408, "请求超时,请稍后重试"); } /** * 截止时间覆盖整个异步操作(含预检和 JSON/Blob 解码),而非只覆盖收到响应头之前。 * 超时会中止 fetch;finally 清理计时器,调用方仍须在 await 后检查取消状态。 */ async function withRequestDeadline( operation, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, controller = new AbortController(), ) { const duration = Number.isFinite(timeoutMs) && timeoutMs > 0 && timeoutMs <= 2_147_483_647 ? timeoutMs : DEFAULT_REQUEST_TIMEOUT_MS; let timer; const deadline = new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(requestTimeoutError()); }, duration); }); try { // The deadline also covers response-body decoding and any purchase preflight. return await Promise.race([ Promise.resolve().then(() => operation(controller.signal)), deadline, ]); } finally { clearTimeout(timer); } } function assertRequestActive(signal) { if (signal.aborted) throw requestTimeoutError(); } // 已分类的业务错误原样传播;浏览器底层网络异常统一为状态 0,避免泄露内部信息。 function normalizeRequestError(error) { return error instanceof ApiError ? error : new ApiError(0, "网络异常,请检查网络后重试"); } /** 在 Nuxt 上下文中创建客户端;apiBase 只配置后端 origin,本模块统一追加接口前缀。 */ function useApi() { const config = useRuntimeConfig(); const auth = useAuth(); // 过期请求的 401 只能清理它所属的会话,不能把随后重新登录的新账号退出。 function clearUnauthorizedSession(requestToken) { if ( !requestToken || localStorage.getItem("jwt") !== requestToken || auth.token !== requestToken ) return; auth.clear(); if (window.location.pathname === "/login") return; const redirect = window.location.pathname + window.location.search; try { void Promise.resolve( navigateTo({ path: "/login", query: redirect === "/" ? undefined : { redirect }, }), ).catch(() => {}); } catch {} } function responseObject(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : {}; } // HTTP 401 优先于业务信封;其他错误优先使用非 200 的业务状态,并脱敏错误消息。 function responseError(response, payload, requestToken, fallbackMessage) { const envelope = responseObject(payload); const status = response.status === 401 ? 401 : typeof envelope.status === "number" && envelope.status !== 200 ? envelope.status : response.status; if (status === 401) clearUnauthorizedSession(requestToken); return new ApiError( status, W(envelope.msg || envelope.message) || fallbackMessage, envelope.data, ); } /** 无 status 的 data 数组仅在调用方明确开启兼容项时接受,其他格式异常均拒绝。 */ async function decodeResponse( response, requestToken, fallbackMessage, signal, allowStatuslessDataArray = false, ) { const payload = await response.json().catch(() => null); assertRequestActive(signal); const envelope = responseObject(payload); if (response.ok && envelope.status === 200) return payload; if ( response.ok && allowStatuslessDataArray && !("status" in envelope) && Array.isArray(envelope.data) ) { return { status: 200, msg: "", data: envelope.data }; } throw responseError(response, payload, requestToken, fallbackMessage); } // 数组参数编码为 key[];默认省略 null/空串,少数筛选接口可显式保留空串。 function apiUrl(path, query, keepEmpty = false) { let url = `${String(config.public.apiBase ?? "")}/console/v1${path}`; const search = new URLSearchParams(); for (const [key, value] of Object.entries(query ?? {})) { if (value == null || (value === "" && !keepEmpty)) continue; if (Array.isArray(value)) { for (const item of value) { if (item != null && item !== "") search.append(`${key}[]`, String(item)); } } else { search.append(key, String(value)); } } if (search.size) url += `?${search}`; return url; } /** JSON 请求主路径:固定本次身份和请求体 → 必要时限购预检 → 请求 → 验证信封。 */ async function request(method, path, options = {}) { // Capture identity before the first await, including when logout clears it immediately. const requestToken = auth.token; const headers = { Accept: "application/json" }; if (requestToken) headers.Authorization = `Bearer ${requestToken}`; if (options.body !== undefined) headers["Content-Type"] = "application/json"; try { const serializedBody = options.body === undefined ? undefined : JSON.stringify(options.body); return await withRequestDeadline(async (signal) => { if (requestToken) { let purchase; try { purchase = Ee(method, path, options.body); } catch (error) { throw new ApiError( 422, error instanceof Error ? error.message : "购买信息无效,请重新确认后重试", ); } if (purchase) { // 等待预检期间身份或表单可能变化;二次比较确保真正提交的是已核验的内容。 const authToken = auth.token; const permission = await request("POST", "/purchase_limit/check", { body: purchase, timeoutMs: options.timeoutMs ?? 15_000, }); assertRequestActive(signal); if ( localStorage.getItem("jwt") !== requestToken || auth.token !== authToken ) { throw new ApiError(409, "登录状态已变化,请重新确认后重试"); } if (JSON.stringify(options.body) !== serializedBody) { throw new ApiError(409, "购买信息已变化,请重新确认后重试"); } if (!De(permission)) throw new ApiError(502, "限购验证暂不可用,请稍后重试"); if (!permission.allowed) throw new ApiError(422, permission.message); } } assertRequestActive(signal); const response = await fetch( apiUrl(path, options.query, options.keepEmpty), { method, headers, body: serializedBody, signal, }, ); assertRequestActive(signal); const envelope = await decodeResponse( response, requestToken, response.ok ? "响应格式异常,请稍后重试" : `请求失败(HTTP ${response.status})`, signal, options.allowStatuslessDataArray, ); assertRequestActive(signal); // 普通写操作清空列表缓存;只读报价和限购预检不应让正在展示的列表无故失效。 if ( method !== "GET" && path !== "/purchase_limit/check" && !/^\/product\/\d+\/config_option$/.test(path) ) { _e(); } return options.raw ? envelope : envelope.data; }, options.timeoutMs); } catch (error) { throw normalizeRequestError(error); } } /** 文件使用 multipart/form-data,由浏览器生成 boundary;不要手动设置 Content-Type。 */ async function upload(file) { const body = new FormData(); body.append("file", file); const headers = { Accept: "application/json" }; const requestToken = auth.token; if (requestToken) headers.Authorization = `Bearer ${requestToken}`; try { return await withRequestDeadline(async (signal) => { const response = await fetch(apiUrl("/upload"), { method: "POST", headers, body, signal, }); assertRequestActive(signal); const envelope = await decodeResponse( response, requestToken, "上传失败", signal, ); assertRequestActive(signal); return envelope.data; }, FILE_TRANSFER_TIMEOUT_MS); } catch (error) { throw normalizeRequestError(error); } } /** 下载仍校验 JSON 错误信封;成功后创建临时下载链接,并在点击后释放对象 URL。 */ async function download(path, data, filename = "export.xlsx", options = {}) { const method = options.method ?? "GET"; const headers = { Accept: "*/*" }; const requestToken = auth.token; if (requestToken) headers.Authorization = `Bearer ${requestToken}`; if (method === "POST") headers["Content-Type"] = "application/json"; try { await withRequestDeadline(async (signal) => { const response = await fetch( apiUrl(path, method === "GET" ? data : undefined), { method, headers, ...(method === "POST" ? { body: JSON.stringify(data ?? {}) } : {}), signal, }, ); assertRequestActive(signal); const isJson = /\bapplication\/(?:[\w.-]+\+)?json\b/i.test( response.headers.get("content-type") || "", ); if (!response.ok || isJson) { const payload = await response .clone() .json() .catch(() => null); assertRequestActive(signal); const envelope = responseObject(payload); if ( !response.ok || (typeof envelope.status === "number" && envelope.status !== 200) ) { throw responseError( response, payload, requestToken, `下载失败(HTTP ${response.status})`, ); } } const disposition = response.headers.get("content-disposition") || ""; const match = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition); if (match?.[1]) { try { filename = decodeURIComponent(match[1]); } catch { filename = match[1]; } } const blob = await response.blob(); assertRequestActive(signal); const objectUrl = URL.createObjectURL(blob); const link = document.createElement("a"); try { link.href = objectUrl; link.download = filename; document.body.appendChild(link); link.click(); } finally { link.remove(); URL.revokeObjectURL(objectUrl); } }, options.timeoutMs ?? FILE_TRANSFER_TIMEOUT_MS); } catch (error) { throw normalizeRequestError(error); } } return { identity: () => auth.token, request, upload, download, get: (path, query, keepEmpty = false, options = {}) => request("GET", path, { ...options, query, keepEmpty, }), getRaw: (path, query, options = {}) => request("GET", path, { ...options, query, raw: true, }), post: (path, body, raw = false, options = {}) => request("POST", path, { ...options, body, raw, }), put: (path, body, query) => request("PUT", path, { body, query }), del: (path, query, body) => request("DELETE", path, { query, body }), }; } // 既有后端登录协议要求固定 AES-CBC 参数;这是传输字段兼容,不是安全密码存储。 // 修改必须与后端协商,HTTPS 仍不可省略,前端公开的固定参数不能视为秘密。 var ot = CryptoJS.enc.Utf8.parse(`idcsmart.finance`); var st = CryptoJS.enc.Utf8.parse(`9311019310287172`); function ct(e) { return CryptoJS.AES.encrypt(e, ot, { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, iv: st, }).toString(); } /** 按安全验证、限流、超时和服务异常分类登录错误,屏蔽 HTML/SQL/堆栈等内部内容。 */ function lt(e, t = `登录失败,请稍后重试`) { let n = e && typeof e == `object` ? e : {}, r = typeof n.message == `string` ? n.message.trim() : ``, i = n.data && typeof n.data == `object` ? n.data : {}; return i.need_security_verify || i.ip_exception_verify ? `本次登录需要额外的安全验证,请联系客服协助完成验证。` : n.status === 429 ? `登录尝试过于频繁,请稍后再试` : n.status === 408 || /超时|timeout/i.test(r) ? `登录请求超时,请检查网络后重试` : n.status === 0 || /网络异常|Failed to fetch|NetworkError|Load failed/i.test(r) ? `网络连接失败,请检查网络后重试` : (n.status ?? 0) >= 500 || /页面错误|响应格式异常|Internal Server Error|Bad Gateway|Service Unavailable/i.test( r, ) ? `登录服务暂时异常,请稍后重试;若持续失败,请联系客服。` : !r || /<[^>]+>|SQLSTATE|\b(?:TypeError|ReferenceError|Exception)\b/.test( r, ) ? t : r; } // 这里只检查凭证形状,不验证签名和权限;真实性须由携带凭证的后端请求确认。 function ut(e) { return ( typeof e == `string` && e.length > 0 && e.length <= 8192 && !/\s/.test(e) ); } /** 从接口成功/失败的常见包装中识别额外安全验证挑战,未找到时返回 null。 */ function dt(e) { if (!e || typeof e != `object`) return null; let t = e, n = [t, t.data, t.error, t.response]; for (let e of n) { if (!e || typeof e != `object`) continue; let t = e, n = t.data && typeof t.data == `object` ? t.data : t; if ( n.need_security_verify === !0 || n.need_security_verify === 1 || n.need_security_verify === `1` || (Array.isArray(n.available_methods) && n.available_methods.length > 0) ) return n; } return null; } // 收到挑战不能被当成业务已完成;将挑战附在异常 data 上交给安全验证对话框。 function ft(e) { let t = dt(e); if (t) throw Object.assign(Error(`安全验证尚未完成,请重试`), { data: t, }); } var pt = { operate_password: `操作密码`, email_code: `邮箱验证码`, phone_code: `手机验证码`, certification: `实名认证`, }; /** 将后端允许的验证方式转成选择项;未知方式不展示,逐项 token 优先于公共 token。 */ function mt(e) { return ( Array.isArray(e.available_methods) ? e.available_methods : [] ).flatMap((t) => { let n = typeof t == `string` ? { value: t, } : t && typeof t == `object` ? t : {}, r = String(n.value ?? ``); return pt[r] ? [ { value: r, label: pt[r], tip: String(n.tip ?? ``), account: String(n.account ?? ``), phone_code: String(n.phone_code ?? ``), security_verify_token: String( n.security_verify_token ?? e.security_verify_token ?? ``, ), }, ] : []; }); } /** * JWT 持久化到 localStorage 是现有会话约定,仍需防范 XSS;不在日志中输出凭证。 * sessionVersion 隔离跨登录的异步结果,overviewPromise 合并同一会话的并发首页请求。 */ const useAuth = defineStore("auth", () => { const token = ref(localStorage.getItem("jwt")); const overview = ref(null); const loading = ref(false); const legacyLoginPending = ref(false); const isLoggedIn = computed(() => !!token.value); let overviewPromise = null; let sessionVersion = 0; let legacySession = null; // 仅在登录或服务端核验成功后调用;先清理上一会话的缓存与未完成旧平台登录。 function loginWithJwt(jwt) { if (!ut(jwt)) throw Error("登录响应无效,请重新登录;若持续失败,请联系客服。"); resetSessionState(); token.value = jwt; localStorage.setItem("jwt", jwt); } // 旧平台带来的凭证先暂存待验证,不立即写成已登录状态,避免路由守卫误放行。 function prepareLegacyLogin(jwt = localStorage.getItem("jwt")) { clear(); legacySession = { jwt: ut(jwt) ? jwt : null, version: sessionVersion, controller: null, promise: null, }; legacyLoginPending.value = true; } // 退出/切换身份会中止旧验证,并丢弃其结果。 function cancelLegacyLogin() { legacySession?.controller?.abort(); legacySession = null; legacyLoginPending.value = false; } /** * 用 /index 实际核验旧平台 JWT;并发点击共享 Promise,失败后允许同一入口重试。 * no-store/omit/no-referrer 避免缓存、Cookie 和 Referer 携带额外身份信息。 */ function verifyLegacyLogin() { const session = legacySession; if (!session?.jwt) { return Promise.reject( Error("缺少有效登录凭证,请返回原平台重新进入,或使用账号登录。"), ); } if (session.promise) return session.promise; const apiBase = String(useRuntimeConfig().public.apiBase ?? ""); const controller = new AbortController(); session.controller = controller; const isCurrentSession = () => legacySession === session && sessionVersion === session.version; const promise = withRequestDeadline( async (signal) => { let response; try { response = await fetch(`${apiBase}/console/v1/index`, { headers: { Accept: "application/json", Authorization: `Bearer ${session.jwt}`, }, credentials: "omit", cache: "no-store", referrerPolicy: "no-referrer", signal, }); } catch { throw Error("网络连接失败或验证超时,请检查网络后重试。"); } const envelope = await response.json().catch(() => null); assertRequestActive(signal); if (!isCurrentSession()) throw Error("登录状态已变化,请重新进入。"); if (response.status === 401 || envelope?.status === 401) { throw Error("登录凭证已失效,请返回原平台重新进入,或使用账号登录。"); } const account = envelope?.data?.account; if ( !response.ok || envelope?.status !== 200 || !account || typeof account !== "object" || !Number.isSafeInteger(account.id) || account.id <= 0 ) throw Error("暂时无法验证登录身份,请稍后重试。"); loginWithJwt(session.jwt); overview.value = account; }, 15_000, controller, ) .catch((error) => { if (error instanceof ApiError && error.status === 408) { throw Error("网络连接失败或验证超时,请检查网络后重试。"); } throw error; }) .finally(() => { if (isCurrentSession()) { session.controller = null; session.promise = null; } }); session.promise = promise; return promise; } /** 密码/手机验证码登录共用入口;安全验证结果由调用方传入,成功后才保存 JWT。 */ async function login( account, password, captcha = "", captchaToken = "", type = "password", code = "", phoneCode = "86", verification, ) { if (type === "code" && account.includes("@")) throw Error("验证码登录仅支持手机号"); const api = useApi(); const version = sessionVersion; loading.value = true; try { const result = await api.post( "/login", { type, account, ...(account.includes("@") ? {} : { phone_code: phoneCode }), code, password: type === "password" ? ct(password) : "", remember_password: "0", captcha, token: captchaToken, ...(verification || {}), }, false, { timeoutMs: 15_000 }, ); if (version !== sessionVersion) throw Error("登录状态已变化,请重试"); ft(result); if (!ut(result?.jwt)) throw Error("登录响应无效,请重新登录;若持续失败,请联系客服。"); loginWithJwt(result.jwt); } finally { loading.value = false; } } // 在请求完成前共享 Promise;会话版本不一致时拒绝回填,防止旧账号资料覆盖新账号。 function loadOverview() { if (!overviewPromise) { const version = sessionVersion; const promise = useApi() .get("/index", undefined, false, { timeoutMs: 10_000 }) .then((result) => { if (version !== sessionVersion) throw Error("登录状态已变化,请重试"); overview.value = result.account; return result.account; }) .finally(() => { if (overviewPromise === promise) overviewPromise = null; }); overviewPromise = promise; } return overviewPromise; } // 页面首次读取允许使用本会话已有资料;需要强制刷新余额等信息时调用 refreshOverview。 async function fetchOverview() { return overview.value ?? loadOverview(); } async function refreshOverview() { return loadOverview(); } // 本地退出立即生效,不等待远端吊销成功;请求已同步捕获旧 Bearer,不会误用新身份。 function logout() { // request() captures the original Bearer synchronously; remote revocation never owns local logout. const revocation = useApi().post("/logout"); clear(); void revocation.catch(() => {}); } // 所有会话切换走同一清理入口,使缓存、旧平台验证和账户资料同时失效。 function resetSessionState() { _e(); cancelLegacyLogin(); sessionVersion += 1; overview.value = null; overviewPromise = null; } function clear() { resetSessionState(); token.value = null; localStorage.removeItem("jwt"); } return { token, overview, loading, isLoggedIn, login, loginWithJwt, fetchOverview, refreshOverview, logout, clear, legacyLoginPending, prepareLegacyLogin, verifyLegacyLogin, cancelLegacyLogin, }; }); export { lt as loginErrorMessage, ApiError, ye as apiCacheKey, ve as apiCacheVersion, W as sanitizeMessage, de as operationMessage, mt as securityMethods, useApi, Se as writeApiCache, ft as assertSecurityComplete, ut as isValidJwt, xe as readApiCache, dt as securityChallenge, useAuth, _e as clearApiCache, };