(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, serviceWorker: root.navigator?.serviceWorker, credentials: root.navigator?.credentials, MessageChannel: root.MessageChannel, location: root.location, addActivityListener: (type, listener, options) => root.addEventListener(type, listener, options), confirmAction: message => root.confirm(message), promptAuthorization: details => root.prompt( `Confirm ${String(details.action || 'this action').replaceAll('_', ' ')} by entering your dashboard access token.`, ), onExpired: () => root.dispatchEvent(new CustomEvent('stackchain:session-expired')), onClearError: error => { root.dispatchEvent(new CustomEvent('stackchain:device-clear-failed', { detail: error.message })); root.alert('Signed out, but private queued work could not be cleared. Close other Stackchain tabs and clear this site’s data.'); }, }); root.fetch = boundary.fetch; const attach = () => { boundary.startActivityHeartbeat(); const button = root.document.getElementById('sign-out'); if (button) button.addEventListener('click', () => boundary.signOut()); const allDevicesButton = root.document.getElementById('sign-out-all'); if (allDevicesButton) allDevicesButton.addEventListener('click', () => boundary.signOutAllDevices()); const devicesButton = root.document.getElementById('active-devices'); const devicesSheet = root.document.getElementById('active-devices-sheet'); const devicesList = root.document.getElementById('active-devices-list'); const devicesStatus = root.document.getElementById('active-devices-status'); const passkeysList = root.document.getElementById('enrolled-passkeys-list'); const passkeysStatus = root.document.getElementById('enrolled-passkeys-status'); const closeDevices = root.document.getElementById('close-active-devices'); const enrollPasskey = root.document.getElementById('enroll-passkey'); const activityList = root.document.getElementById('security-activity-list'); const activityStatus = root.document.getElementById('security-activity-status'); const loadMoreActivity = root.document.getElementById('load-more-security-activity'); let activityCursor = null; const renderDevices = async () => { devicesStatus.textContent = 'Loading active devices…'; devicesList.replaceChildren(); try { const devices = await boundary.listActiveDevices(); devices.forEach(device => { const row = root.document.createElement('article'); row.className = 'active-device'; const details = root.document.createElement('div'); const label = root.document.createElement('strong'); label.textContent = device.device_label; const timing = root.document.createElement('span'); timing.className = 'small muted'; timing.textContent = `Signed in ${new Date(device.created_at * 1000).toLocaleString()} · expires ${new Date(device.expires_at * 1000).toLocaleString()}`; details.append(label, timing); if (device.current) { const current = root.document.createElement('span'); current.className = 'active-device-current'; current.textContent = 'This device'; details.append(current); } const revoke = root.document.createElement('button'); revoke.type = 'button'; revoke.textContent = device.current ? 'Sign out' : 'Revoke'; revoke.addEventListener('click', async () => { if (device.current) await boundary.signOut(); else if (await boundary.revokeActiveDevice(device)) await renderDevices(); }); row.append(details, revoke); devicesList.append(row); }); devicesStatus.textContent = devices.length ? `${devices.length} active device${devices.length === 1 ? '' : 's'}` : 'No active devices.'; } catch (_error) { devicesStatus.textContent = 'Active devices could not be loaded. Try again.'; } }; const renderPasskeys = async () => { if (!passkeysList || !passkeysStatus) return; passkeysStatus.textContent = 'Loading enrolled passkeys…'; passkeysList.replaceChildren(); try { const enrolled = await boundary.listPasskeys(); enrolled.forEach(passkey => { const row = root.document.createElement('article'); row.className = 'enrolled-passkey'; const details = root.document.createElement('div'); const label = root.document.createElement('strong'); label.textContent = passkey.device_label; const timing = root.document.createElement('span'); timing.className = 'small muted'; const state = passkey.current ? 'This active device' : (passkey.active ? 'Active device' : 'No active session'); timing.textContent = `Enrolled ${new Date(passkey.created_at * 1000).toLocaleString()} · ${state}`; details.append(label, timing); const remove = root.document.createElement('button'); remove.type = 'button'; remove.textContent = 'Remove passkey'; remove.addEventListener('click', async () => { remove.disabled = true; passkeysStatus.textContent = `Removing passkey for ${passkey.device_label}…`; try { const outcome = await boundary.revokePasskey(passkey); if (outcome) { passkeysStatus.textContent = outcome.current_session ? 'Passkey removed. This session remains active; keep your recovery token available.' : 'Passkey removed.'; await renderPasskeys(); if (outcome.session_revoked) await renderDevices(); } else { remove.disabled = false; passkeysStatus.textContent = `${enrolled.length} enrolled passkey${enrolled.length === 1 ? '' : 's'}`; } } catch (_error) { passkeysStatus.textContent = 'Passkey could not be removed. Refresh to verify before retrying.'; remove.disabled = false; } }); row.append(details, remove); passkeysList.append(row); }); passkeysStatus.textContent = enrolled.length ? `${enrolled.length} enrolled passkey${enrolled.length === 1 ? '' : 's'}` : 'No passkeys enrolled.'; } catch (_error) { passkeysStatus.textContent = 'Enrolled passkeys could not be loaded. Try again.'; } }; const renderSecurityActivity = async (append = false) => { if (!activityList || !activityStatus || !loadMoreActivity) return; activityStatus.textContent = append ? 'Loading older activity…' : 'Loading security activity…'; loadMoreActivity.hidden = true; if (!append) { activityCursor = null; activityList.replaceChildren(); } try { const page = await boundary.listSecurityEvents(append ? activityCursor : null); if (!append) { page.authentication_alerts.forEach(alert => { const row = root.document.createElement('article'); row.className = 'security-event authentication-alert'; const formatted = boundary.formatAuthenticationAlert(alert); const title = root.document.createElement('strong'); title.textContent = formatted.title; const details = root.document.createElement('span'); details.className = 'small muted'; details.textContent = formatted.detail; row.append(title, details); activityList.append(row); }); } const labels = { sign_in: 'Signed in', sign_out: 'Signed out', passkey_enrolled: 'Passkey enrolled', device_revoked: 'Device access revoked', all_sessions_revoked: 'All device access revoked', issue_closed: 'Issue closed', pull_merged: 'Pull request merged', pull_review_approved: 'Pull request approved', pull_review_changes_requested: 'Changes requested', gitea_time_logged: 'Gitea time logged', }; page.events.forEach(event => { const row = root.document.createElement('article'); row.className = 'security-event'; const title = root.document.createElement('strong'); title.textContent = labels[event.kind] || 'Security event'; const details = root.document.createElement('span'); details.className = 'small muted'; const context = [ event.device_label, event.method, event.target, event.status === 'pending' ? 'Outcome confirmation pending' : null, ].filter(value => typeof value === 'string' && value).join(' · '); details.textContent = `${new Date(event.created_at * 1000).toLocaleString()}${context ? ' · ' + context : ''}`; row.append(title, details); activityList.append(row); }); activityCursor = page.next_cursor; activityStatus.textContent = activityList.children.length ? `${activityList.children.length} recent security event${activityList.children.length === 1 ? '' : 's'}` : 'No security activity yet.'; loadMoreActivity.textContent = 'Load older activity'; loadMoreActivity.hidden = !activityCursor; } catch (_error) { activityStatus.textContent = 'Security activity could not be loaded.'; loadMoreActivity.textContent = 'Retry activity'; loadMoreActivity.hidden = false; } }; loadMoreActivity?.addEventListener('click', () => renderSecurityActivity(Boolean(activityCursor))); if (devicesButton && devicesSheet) devicesButton.addEventListener('click', () => { devicesSheet.hidden = false; closeDevices?.focus(); renderDevices(); renderPasskeys(); renderSecurityActivity(); }); if (closeDevices && devicesSheet) closeDevices.addEventListener('click', () => { devicesSheet.hidden = true; devicesButton?.focus(); }); enrollPasskey?.addEventListener('click', async () => { enrollPasskey.disabled = true; devicesStatus.textContent = 'Waiting for your device passkey…'; try { await boundary.enrollPasskey(); devicesStatus.textContent = 'Passkey enrolled. You can use it at sign-in and authorization prompts.'; await renderPasskeys(); } catch (_error) { devicesStatus.textContent = 'Passkey enrollment was not completed. Try again.'; } finally { enrollPasskey.disabled = false; } }); boundary.refreshOfflineLease().then(valid => { if (valid) boundary.resumeQueuedWork(); }); }; root.navigator?.serviceWorker?.addEventListener?.('message', event => boundary.handleServiceWorkerMessage(event)); 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, serviceWorker, credentials, MessageChannel, location, confirmAction, addActivityListener, promptAuthorization = () => null, onExpired = () => {}, onClearError = () => {}, requestTimeoutMs = 15000, activityHeartbeatIntervalMs = 60000, now = () => Date.now(), setTimer = (callback, delay) => setTimeout(callback, delay), }) { const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']); let expirationStarted = false; let idleLockStarted = false; let lastActivityHeartbeatAt = Number.NEGATIVE_INFINITY; let activityHeartbeat = null; async function handleRemoteRevocation() { if (expirationStarted) return; expirationStarted = true; await clearPrivateDeviceData(); location.replace(base + 'login?reason=session-revoked'); } async function handleSessionExpiry() { if (expirationStarted) return; expirationStarted = true; await clearPrivateDeviceData(); onExpired(); location.replace(base + 'login?reason=session-expired'); } function handleSessionIdle() { if (expirationStarted || idleLockStarted) return; idleLockStarted = true; location.replace(base + 'login?reason=session-idle'); } async function handleServiceWorkerMessage(event) { if (event?.data?.type === 'stackchain-session-revoked') { await handleRemoteRevocation(); } if (event?.data?.type === 'stackchain-session-idle') { handleSessionIdle(); } if (event?.data?.type === 'stackchain-session-expired') { await handleSessionExpiry(); } } function storedOfflineLease() { const value = Number(localStorage?.getItem?.('stackchain.session-expires-at')); return Number.isInteger(value) && value > 0 ? value : 0; } function storedOfflineIdleLease() { const value = Number(localStorage?.getItem?.('stackchain.session-idle-expires-at')); return Number.isInteger(value) && value > 0 ? value : 0; } function scheduleOfflineExpiry(expiresAt) { const delay = Math.max(0, expiresAt * 1000 - now()); setTimer(() => handleSessionExpiry(), delay); } function scheduleOfflineIdle(idleExpiresAt) { const delay = Math.max(0, idleExpiresAt * 1000 - now()); setTimer(() => { if (storedOfflineIdleLease() * 1000 <= now()) handleSessionIdle(); }, delay); } async function publishOfflineLease(expiresAt, idleExpiresAt) { localStorage?.setItem?.('stackchain.session-expires-at', String(expiresAt)); localStorage?.setItem?.('stackchain.session-idle-expires-at', String(idleExpiresAt)); scheduleOfflineExpiry(expiresAt); scheduleOfflineIdle(idleExpiresAt); const registration = await serviceWorker?.ready; registration?.active?.postMessage({ type: 'stackchain-session-lease', expiresAt, idleExpiresAt, }); } 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 decodeBase64Url(value) { const padded = String(value).replaceAll('-', '+').replaceAll('_', '/') + '='.repeat((4 - String(value).length % 4) % 4); return Uint8Array.from(atob(padded), character => character.charCodeAt(0)); } function encodeBase64Url(value) { if (value === null || value === undefined) return null; let binary = ''; new Uint8Array(value).forEach(byte => { binary += String.fromCharCode(byte); }); return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', ''); } function authenticationCredentialJSON(credential) { return { id: credential.id, type: credential.type, rawId: encodeBase64Url(credential.rawId), response: { authenticatorData: encodeBase64Url(credential.response.authenticatorData), clientDataJSON: encodeBase64Url(credential.response.clientDataJSON), signature: encodeBase64Url(credential.response.signature), userHandle: encodeBase64Url(credential.response.userHandle), }, }; } function isSameOrigin(input) { try { return new URL(String(input?.url || input), origin).origin === origin; } catch (_error) { return false; } } function isDashboardApi(input) { try { const url = new URL(String(input?.url || input), origin); return url.origin === origin && url.pathname.startsWith(base + 'api/v1/'); } catch (_error) { return false; } } async function fetchWithDeadline(input, options, method, phase = 'request') { if (!isDashboardApi(input)) return fetchImpl(input, options); const controller = new AbortController(); const callerSignal = options.signal || input?.signal; let timeout; let rejectCancellation; const cancellation = new Promise((_resolve, reject) => { rejectCancellation = reject; }); const cancelFromCaller = () => { const error = callerSignal.reason || new DOMException('The request was aborted.', 'AbortError'); error.source = 'caller'; controller.abort(error); rejectCancellation(error); }; if (callerSignal?.aborted) cancelFromCaller(); else callerSignal?.addEventListener?.('abort', cancelFromCaller, { once: true }); const deadline = new Promise((_resolve, reject) => { timeout = setTimeout(() => { const error = new Error( SAFE_METHODS.has(method) ? 'Request timed out. Try again.' : 'Request timed out. Refresh to verify the outcome before retrying.' ); error.name = 'TimeoutError'; error.method = method; error.outcome = 'unknown'; error.safeToRetry = SAFE_METHODS.has(method); error.phase = phase; controller.abort(error); reject(error); }, requestTimeoutMs); }); try { return await Promise.race([ Promise.resolve().then(() => fetchImpl(input, { ...options, signal: controller.signal })), deadline, cancellation, ]); } finally { clearTimeout(timeout); callerSignal?.removeEventListener?.('abort', cancelFromCaller); } } async function authorizeWithPasskey(details) { if (!credentials?.get) return null; const headers = new Headers({ Accept: 'application/json', 'Content-Type': 'application/json' }); const csrf = csrfToken(); if (csrf) headers.set('X-CSRF-Token', csrf); const optionsResponse = await fetchWithDeadline(base + 'api/v1/passkeys/authorization/options', { method: 'POST', headers, body: JSON.stringify({ action: details.action, target: details.target }), }, 'POST', 'passkey-authorization'); if (!optionsResponse.ok) return null; const publicKey = await optionsResponse.json(); const challenge = publicKey.challenge; publicKey.challenge = decodeBase64Url(publicKey.challenge); publicKey.allowCredentials = (publicKey.allowCredentials || []).map(item => ({ ...item, id: decodeBase64Url(item.id), })); const credential = await credentials.get({ publicKey }); const verified = await fetchWithDeadline(base + 'api/v1/passkeys/authorization/verify', { method: 'POST', headers, body: JSON.stringify({ challenge, credential: authenticationCredentialJSON(credential), action: details.action, target: details.target, }), }, 'POST', 'passkey-authorization'); if (!verified.ok) return null; const payload = await verified.json().catch(() => ({})); return payload.grant || null; } async function sessionFetch(input, options = {}, allowStepUp = true) { 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 fetchWithDeadline(input, requestOptions, method); if (response.status === 428 && isSameOrigin(input) && allowStepUp) { const payload = await response.clone().json().catch(() => ({})); const detail = payload?.detail || {}; if (detail.code === 'step_up_required' && detail.action && detail.target) { let grant = null; try { grant = await authorizeWithPasskey(detail); } catch (_error) { /* Cancellation and unavailable passkeys fall back to recovery. */ } if (!grant) { const accessToken = await promptAuthorization({ action: detail.action, target: detail.target, }); if (!accessToken) return response; const authorizationHeaders = new Headers({ Accept: 'application/json', 'Content-Type': 'application/json', }); const csrf = csrfToken(); if (csrf) authorizationHeaders.set('X-CSRF-Token', csrf); const authorization = await fetchWithDeadline(base + 'api/v1/fresh-authorization', { method: 'POST', headers: authorizationHeaders, body: JSON.stringify({ access_token: accessToken, action: detail.action, target: detail.target, }), }, 'POST', 'fresh-authorization'); if (!authorization.ok) return authorization; const authorizationPayload = await authorization.json().catch(() => ({})); grant = authorizationPayload.grant; } if (!grant) return response; const retryHeaders = new Headers(requestOptions.headers || input?.headers || {}); retryHeaders.set('X-Step-Up-Grant', grant); return sessionFetch(input, { ...requestOptions, headers: retryHeaders }, false); } } if ( response.status === 401 && isSameOrigin(input) && !expirationStarted && !idleLockStarted ) { expirationStarted = true; const payload = await response.clone().json().catch(() => ({})); if (payload.code === 'session_revoked') { expirationStarted = false; await handleRemoteRevocation(); } else if (payload.code === 'session_idle') { expirationStarted = false; handleSessionIdle(); } else { expirationStarted = false; await handleSessionExpiry(); } } return response; } function recordActivity() { const current = now(); if ( expirationStarted || idleLockStarted || activityHeartbeat || current - lastActivityHeartbeatAt < activityHeartbeatIntervalMs ) return Promise.resolve(false); lastActivityHeartbeatAt = current; activityHeartbeat = sessionFetch(base + 'api/v1/session/activity', { method: 'POST' }) .then(async response => { if (!response.ok) return false; const payload = await response.json().catch(() => ({})); const expiresAt = storedOfflineLease(); if (expiresAt && Number.isInteger(payload.idle_expires_at)) { await publishOfflineLease(expiresAt, payload.idle_expires_at); } return true; }) .catch(() => false) .finally(() => { activityHeartbeat = null; }); return activityHeartbeat; } function startActivityHeartbeat() { ['pointerdown', 'keydown', 'touchstart'].forEach(type => { addActivityListener?.(type, recordActivity, { passive: true }); }); } async function refreshOfflineLease() { try { const response = await sessionFetch(base + 'api/v1/session'); if (!response.ok) return false; const payload = await response.json().catch(() => ({})); if ( !payload.authenticated || !Number.isInteger(payload.expires_at) || !Number.isInteger(payload.idle_expires_at) ) return false; await publishOfflineLease(payload.expires_at, payload.idle_expires_at); return true; } catch (_error) { const expiresAt = storedOfflineLease(); if (expiresAt * 1000 <= now()) { if (expiresAt) await handleSessionExpiry(); return false; } const idleExpiresAt = storedOfflineIdleLease(); if (!idleExpiresAt || idleExpiresAt * 1000 <= now()) { handleSessionIdle(); return false; } scheduleOfflineExpiry(expiresAt); scheduleOfflineIdle(idleExpiresAt); return true; } } 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 stopWorkerOutbox() { const registration = await serviceWorker?.ready; if (!registration?.active || !MessageChannel) return; await new Promise((resolve, reject) => { const channel = new MessageChannel(); const timeout = setTimeout(() => reject(new Error('Background outbox purge timed out.')), 3000); channel.port1.onmessage = event => { clearTimeout(timeout); if (event.data?.ok) resolve(); else reject(new Error(event.data?.error || 'Background outbox purge failed.')); }; registration.active.postMessage({ type: 'stackchain-purge-outbox' }, [channel.port2]); }); } function deletePrivateDatabase(name) { if (!indexedDB) return Promise.resolve(); return new Promise((resolve, reject) => { let request; try { request = indexedDB.deleteDatabase(name); } catch (error) { reject(error); return; } request.onsuccess = () => resolve(); request.onerror = () => reject(request.error || new Error('IndexedDB deletion failed.')); request.onblocked = () => reject(new Error('IndexedDB deletion was blocked.')); }); } async function clearPrivateDeviceData() { removeDashboardStorage(localStorage); if (sessionStorage !== localStorage) removeDashboardStorage(sessionStorage); try { await stopWorkerOutbox(); await deletePrivateDatabase('stackchain-background-outbox-v1'); await deletePrivateDatabase('stackchain-offline-work-v2'); } catch (_error) { const error = new Error('Could not clear private queued work from this device.'); onClearError(error); throw error; } 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 enrollPasskey() { if (!credentials?.create) throw new Error('Passkeys are not supported'); const optionsResponse = await sessionFetch( base + 'api/v1/passkeys/registration/options', { method: 'POST' } ); if (!optionsResponse.ok) throw new Error('Could not start passkey enrollment'); const publicKey = await optionsResponse.json(); const challenge = publicKey.challenge; publicKey.challenge = decodeBase64Url(publicKey.challenge); publicKey.user.id = decodeBase64Url(publicKey.user.id); publicKey.excludeCredentials = (publicKey.excludeCredentials || []).map(item => ({ ...item, id: decodeBase64Url(item.id), })); const credential = await credentials.create({ publicKey }); const response = await sessionFetch(base + 'api/v1/passkeys/registration/verify', { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ challenge, credential: { id: credential.id, type: credential.type, rawId: encodeBase64Url(credential.rawId), response: { attestationObject: encodeBase64Url(credential.response.attestationObject), clientDataJSON: encodeBase64Url(credential.response.clientDataJSON), transports: credential.response.getTransports?.() || [], }, }, }), }); if (!response.ok) throw new Error('Could not verify passkey enrollment'); return true; } async function listActiveDevices() { const response = await sessionFetch(base + 'api/v1/sessions'); if (!response.ok) throw new Error('Could not load active devices'); const payload = await response.json(); return Array.isArray(payload.devices) ? payload.devices : []; } async function listPasskeys() { const response = await sessionFetch(base + 'api/v1/passkeys'); if (!response.ok) throw new Error('Could not load enrolled passkeys'); const payload = await response.json(); return Array.isArray(payload.passkeys) ? payload.passkeys : []; } async function revokePasskey(passkey) { if (!passkey?.management_id) return false; const confirmed = confirmAction?.( `Remove the passkey for ${passkey.device_label}? This cannot be undone.` ); if (!confirmed) return false; const response = await sessionFetch( base + 'api/v1/passkeys/' + encodeURIComponent(passkey.management_id), { method: 'DELETE' }, ); if (!response.ok) throw new Error('Could not remove enrolled passkey'); return response.json(); } function formatAuthenticationAlert(alert) { const failed = Math.max(0, Number(alert?.failed_count) || 0); const blocked = Math.max(0, Number(alert?.blocked_count) || 0); const method = alert?.method === 'passkey' ? 'passkey' : 'token'; const title = `${failed} failed ${method} sign-in${failed === 1 ? '' : 's'}` + (blocked ? ` · ${blocked} blocked` : ''); const formatTime = value => new Date(Number(value) * 1000).toLocaleString( 'en-US', { timeZone: 'UTC' }, ); return { title, detail: `${formatTime(alert?.first_at)} – ${formatTime(alert?.last_at)}`, }; } async function listSecurityEvents(cursor = null) { const query = new URLSearchParams({ limit: '25' }); if (Number.isInteger(cursor) && cursor > 0) query.set('cursor', String(cursor)); const response = await sessionFetch(base + 'api/v1/security-events?' + query); if (!response.ok) throw new Error('Could not load security activity'); const payload = await response.json(); return { events: Array.isArray(payload.events) ? payload.events : [], authentication_alerts: Array.isArray(payload.authentication_alerts) ? payload.authentication_alerts : [], next_cursor: Number.isInteger(payload.next_cursor) ? payload.next_cursor : null, }; } async function revokeActiveDevice(device) { if (!device?.management_id || device.current) return false; const confirmed = confirmAction?.(`Sign out ${device.device_label}?`); if (!confirmed) return false; const response = await sessionFetch( base + 'api/v1/sessions/' + encodeURIComponent(device.management_id), { method: 'DELETE' }, ); if (!response.ok) throw new Error('Could not revoke active device'); return true; } async function signOut() { try { await sessionFetch(base + 'api/v1/session', { method: 'DELETE' }); } finally { await clearPrivateDeviceData(); location.assign(base + 'login'); } } async function signOutAllDevices() { const confirmed = confirmAction?.('Sign out every device? You will need to sign in again everywhere.'); if (!confirmed) return false; const response = await sessionFetch(base + 'api/v1/sessions', { method: 'DELETE' }); if (!response.ok) throw new Error('Could not sign out all devices'); await clearPrivateDeviceData(); location.assign(base + 'login'); return true; } async function resumeQueuedWork() { try { const registration = await serviceWorker?.ready; registration?.active?.postMessage({ type: 'stackchain-resume-outbox' }); } catch (_error) { /* Background Sync is optional; foreground delivery remains available. */ } } return { fetch: sessionFetch, signOut, signOutAllDevices, listActiveDevices, listPasskeys, listSecurityEvents, formatAuthenticationAlert, enrollPasskey, revokeActiveDevice, revokePasskey, clearPrivateDeviceData, handleServiceWorkerMessage, refreshOfflineLease, resumeQueuedWork, recordActivity, startActivityHeartbeat, }; });