(function (root, factory) { if (typeof module !== 'undefined' && module.exports) module.exports = factory; else { const base = new URL('./', root.location.href).pathname; const originalFetch = root.fetch.bind(root); const boundary = factory({ cookie: () => root.document.cookie, origin: root.location.origin, base, fetchImpl: originalFetch, localStorage: root.localStorage, sessionStorage: root.sessionStorage, indexedDB: root.indexedDB, caches: root.caches, location: root.location, onExpired: () => root.dispatchEvent(new CustomEvent('stackchain:session-expired')), }); root.fetch = boundary.fetch; const attach = () => { const button = root.document.getElementById('sign-out'); if (button) button.addEventListener('click', () => boundary.signOut()); }; if (root.document.readyState === 'loading') root.document.addEventListener('DOMContentLoaded', attach); else attach(); root.stackchainSession = boundary; } })(typeof window !== 'undefined' ? window : this, function createSessionBoundary({ cookie, origin, base, fetchImpl, localStorage, sessionStorage, indexedDB, caches, location, onExpired = () => {}, }) { const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']); function csrfToken() { const entry = String(cookie?.() || '').split(';') .map(value => value.trim()) .find(value => value.startsWith('stackchain_csrf=')); return entry ? decodeURIComponent(entry.slice('stackchain_csrf='.length)) : ''; } function isSameOrigin(input) { try { return new URL(String(input?.url || input), origin).origin === origin; } catch (_error) { return false; } } async function sessionFetch(input, options = {}) { const method = String(options.method || input?.method || 'GET').toUpperCase(); const requestOptions = { ...options }; if (!SAFE_METHODS.has(method) && isSameOrigin(input)) { const headers = new Headers(options.headers || input?.headers || {}); const csrf = csrfToken(); if (csrf) headers.set('X-CSRF-Token', csrf); requestOptions.headers = headers; } const response = await fetchImpl(input, requestOptions); if (response.status === 401) onExpired(); return response; } function removeDashboardStorage(storage) { if (!storage) return; const keys = []; try { for (let index = 0; index < storage.length; index += 1) { const key = storage.key(index); if (key?.startsWith('stackchain.')) keys.push(key); } keys.forEach(key => storage.removeItem(key)); } catch (_error) { /* Cookie invalidation still protects server data. */ } } async function clearPrivateDeviceData() { removeDashboardStorage(localStorage); if (sessionStorage !== localStorage) removeDashboardStorage(sessionStorage); try { indexedDB?.deleteDatabase('stackchain-background-outbox-v1'); } catch (_error) { /* Continue clearing other dashboard state. */ } try { const keys = await caches?.keys?.() || []; await Promise.all(keys.filter(key => key.startsWith('stackchain-dashboard-')).map(key => caches.delete(key))); } catch (_error) { /* A later service-worker activation can clear stale caches. */ } } async function signOut() { try { await sessionFetch(base + 'api/v1/session', { method: 'DELETE' }); } finally { await clearPrivateDeviceData(); location.assign(base + 'login'); } } return { fetch: sessionFetch, signOut, clearPrivateDeviceData }; });