(function (root, factory) {
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = options => factory({
      ...options,
      privateDatabases: require('./private-data-registry.js'),
    });
  }
  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,
      privateDatabases: root.stackchainPrivateDatabases,
      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();
      boundary.startReconnectResume();
      const button = root.document.getElementById('sign-out');
      const allButton = root.document.getElementById('sign-out-all');
      let review;
      const features = root.createFeatureLoader({
        document: root.document,
        urls: {
          'sign-out': root.document.querySelector('meta[name="stackchain-feature-sign-out"]')?.content || '',
          'security-center': root.document.querySelector('meta[name="stackchain-feature-security-center"]')?.content || '',
        },
      });
      const openReview = (mode, source) => features.run(
        'sign-out', { trigger: source }, () => {
          if (!review) review = root.createSignOutReview.mount(boundary);
          review.show(mode, source);
        },
      );
      [[button, 'current'], [allButton, 'all']].forEach(([source, mode]) =>
        source?.addEventListener('click', () => openReview(mode, source))
      );
      boundary.requestSignOut = source => openReview('current', source || button);
      const devicesButton = root.document.getElementById('active-devices');
      const devicesSheet = root.document.getElementById('active-devices-sheet');
      const devicesStatus = root.document.getElementById('active-devices-status');
      const closeDevices = root.document.getElementById('close-active-devices');
      const returnFromSecurity = root.document.getElementById('return-from-security-center');
      const beginSecurityDetour = () => {
        if (root.matchMedia?.('(max-width: 600px)')?.matches) {
          root.stackchainTodayTimerView?.beginDetour?.('security-center');
        }
      };
      let loadingSecurityCenter = false;
      const openSecurityCenter = async () => {
        if (loadingSecurityCenter) return;
        loadingSecurityCenter = true;
        beginSecurityDetour();
        devicesSheet.hidden = false;
        closeDevices?.focus();
        try {
          await features.run('security-center', {
            trigger: devicesButton,
            status: devicesStatus,
            retryLabel: 'Tap Active devices to retry.',
          }, () => {
            devicesButton.removeEventListener('click', openSecurityCenter);
            root.attachSecurityCenter(boundary, {
              onOpen: beginSecurityDetour,
              onClose: () => root.stackchainTodayTimerView?.finishDetour?.('security-center'),
            }).open();
          });
        } finally {
          loadingSecurityCenter = false;
        }
      };
      if (devicesButton && devicesSheet) {
        devicesButton.addEventListener('click', openSecurityCenter);
      }
      if (closeDevices && devicesSheet) closeDevices.addEventListener('click', () => {
        devicesSheet.hidden = true;
        devicesButton?.focus();
        root.stackchainTodayTimerView?.finishDetour?.('security-center');
      });
      if (returnFromSecurity && devicesSheet) returnFromSecurity.addEventListener('click', () => {
        if (root.history?.state?.stackchainSecuritySection) root.history.back();
        else devicesSheet.hidden = true;
      });
      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, privateDatabases, 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 });
    });
  }

  function startReconnectResume() {
    addActivityListener?.('online', async () => {
      if (await refreshOfflineLease()) await resumeQueuedWork();
    });
  }

  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();
      for (const name of privateDatabases) await deletePrivateDatabase(name);
    } 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({ reviewed = false } = {}) {
    const confirmed = reviewed || 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,
    startReconnectResume,
    recordActivity,
    startActivityHeartbeat,
  };
});
