(function (root, factory) { if (typeof module === 'object' && module.exports) module.exports = factory; else root.createLoginController = factory; }(typeof self !== 'undefined' ? self : this, function createLoginController(options) { function validShareContinuation(value) { if (typeof value !== 'string' || !value.startsWith('./?') || value.includes('#')) return './'; const limits = { title: 200, text: 8000, url: 2048 }; const params = new URLSearchParams(value.slice(3)); const entries = Array.from(params.entries()); if (!entries.length) return './'; const invalid = entries.some(([name, content]) => ( !Object.prototype.hasOwnProperty.call(limits, name) || !content || content.length > limits[name] )); if (invalid) return './'; return value; } const form = options.form; const status = options.status; const button = options.button; const fetchImpl = options.fetchImpl; const location = options.location; const clearPrivateDeviceData = options.clearPrivateDeviceData; const continuation = validShareContinuation(options.continuation); const setIntervalImpl = options.setIntervalImpl || setInterval; const clearIntervalImpl = options.clearIntervalImpl || clearInterval; let timer = null; if (continuation !== './') status.textContent = 'Sign in to continue your shared capture.'; async function showReason(reason) { if (reason === 'session-expired') { status.textContent = 'Your session expired. Private drafts remain on this device. Sign in to continue.'; return; } if (reason === 'session-idle') { status.textContent = 'Stackchain locked after inactivity. Your drafts and queued work are still on this device. Sign in to resume.'; return; } if (reason !== 'session-revoked') return; button.disabled = true; status.textContent = 'This device was remotely signed out. Clearing Stackchain private data…'; try { if (typeof clearPrivateDeviceData !== 'function') throw new Error('Private data purger unavailable.'); await clearPrivateDeviceData(); status.textContent = 'This device was remotely signed out. Stackchain private data was cleared. Sign in to use it again.'; button.disabled = false; } catch (_error) { status.textContent = 'This device was remotely signed out, but private data could not be cleared. Close other Stackchain tabs and clear this site’s data before signing in.'; } } function showRetryCountdown(seconds) { let remaining = Math.max(1, Number.parseInt(seconds, 10) || 1); button.disabled = true; status.textContent = `Too many attempts. Try again in ${remaining} seconds.`; timer = setIntervalImpl(() => { remaining -= 1; if (remaining <= 0) { clearIntervalImpl(timer); timer = null; button.disabled = false; status.textContent = 'You can try signing in again.'; return; } status.textContent = `Too many attempts. Try again in ${remaining} seconds.`; }, 1000); } async function submit(accessToken, deviceLabel = 'This device') { status.textContent = 'Signing in…'; let response; try { response = await fetchImpl('api/v1/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ access_token: accessToken, device_label: deviceLabel }), }); } catch (_error) { form.reset(); status.textContent = 'Sign-in failed. Check your connection and try again.'; return; } form.reset(); if (response.ok) { location.replace(continuation); return; } if (response.status === 429) { showRetryCountdown(response.headers.get('Retry-After')); return; } status.textContent = 'Sign-in failed. Check the token and try again.'; } return { submit, showReason }; })); if (typeof document !== 'undefined') { const form = document.getElementById('sign-in'); const status = document.getElementById('status'); const button = document.getElementById('submit-sign-in'); const loginParams = new URLSearchParams(window.location.search); const controller = createLoginController({ form, status, button, fetchImpl: fetch.bind(window), location: window.location, continuation: loginParams.get('continue'), clearPrivateDeviceData: window.stackchainPrivateDeviceData, }); controller.showReason(loginParams.get('reason')); form.addEventListener('submit', event => { event.preventDefault(); const data = new FormData(form); const accessToken = data.get('access_token'); const deviceLabel = data.get('device_label'); controller.submit(accessToken, deviceLabel); }); }