/** SiteForge session/API boundary. Do not use Art demo login tokens as SiteForge credentials. */ export interface SiteRef { id:string; name:string; domain?:string } export interface SiteForgeCapabilities { role:string; view:boolean; edit:boolean; review:boolean; deploy:boolean; manage_members:boolean; create_site:boolean; manage_users:boolean } export interface SiteForgeBootstrap { version:string; user:string; superadmin:boolean; sites:SiteRef[]; selected_site_id:string|null; capabilities:SiteForgeCapabilities; navigation:Array<{key:string;title:string;path:string;requires_site:boolean}> } let csrfCache:string|undefined const key='siteforge:art-selected-site' export class SiteForgeError extends Error { constructor(message:string,readonly status:number){super(message);this.name='SiteForgeError'} } export async function sfApi(path:string, init:RequestInit={}):Promise{ if(!path.startsWith('/api/') || path.startsWith('//'))throw new Error('仅允许调用本站 SiteForge API') const method=(init.method||'GET').toUpperCase() const headers=new Headers(init.headers) if(!['GET','HEAD'].includes(method)){ if(!csrfCache){const me=await sfApi<{csrf:string}>('/api/auth/me');csrfCache=me.csrf} headers.set('X-SiteForge-CSRF',csrfCache) } const res=await fetch(path,{...init,method,headers,credentials:'same-origin',redirect:'follow'}) if(res.status===401){csrfCache=undefined;throw new SiteForgeError('SiteForge 登录已过期,请从统一登录页重新登录',401)} const contentType=res.headers.get('content-type')||'' if(!contentType.includes('application/json'))throw new SiteForgeError('接口返回了非 JSON 响应,请检查同域 API 代理配置',502) const data=await res.json() if(!res.ok){ const detail=typeof data?.detail==='string'?data.detail:JSON.stringify(data?.detail||'请求失败') throw new SiteForgeError(detail,res.status) } return data as T } export async function session(){const me=await sfApi<{csrf:string;user:string}>('/api/auth/me');csrfCache=me.csrf;return me} export function preferredSite(sites:T[]):string{ const params=new URLSearchParams(window.location.search) const proposed=params.get('site') || sessionStorage.getItem(key) || '' const chosen=sites.find(s=>s.id===proposed) || sites[0] if(chosen)sessionStorage.setItem(key,chosen.id) return chosen?.id||'' } export function rememberSite(siteId:string){if(siteId)sessionStorage.setItem(key,siteId)} export async function bootstrap():Promise{ const preferred=sessionStorage.getItem(key)||'' try { return await sfApi('/api/console/bootstrap'+(preferred?'?site_id='+encodeURIComponent(preferred):'')) } catch(e){ if(e instanceof SiteForgeError && e.status===403){sessionStorage.removeItem(key);return sfApi('/api/console/bootstrap')} throw e } } export function postJson(body:unknown):RequestInit{return {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}} export function patchJson(body:unknown):RequestInit{return {method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}}