(function () { 'use strict'; var STORAGE_KEY = 'opts'; var DEFAULTS = { mode: 'memorable', words: 4, digits: 2, digitSeparator: '', separator: '-', capitalization: 'none', list: 'eff-large', length: 20, useLower: true, useUpper: true, useDigits: true, useSymbols: false, customSymbols: '', excludeAmbiguous: false, requireEach: true, count: 1 }; var FIELDS = [ { key: 'words', id: 'words', type: 'int', min: 2, max: 10 }, { key: 'digits', id: 'digits', type: 'int', min: 0, max: 4 }, { key: 'digitSeparator', id: 'digitSeparator', type: 'str' }, { key: 'separator', id: 'separator', type: 'str' }, { key: 'capitalization', id: 'capitalization', type: 'str' }, { key: 'list', id: 'list', type: 'str' }, { key: 'length', id: 'length', type: 'int', min: 8, max: 64 }, { key: 'count', id: 'count', type: 'int', min: 1, max: 10 }, { key: 'customSymbols', id: 'customSymbols', type: 'str' }, { key: 'useLower', id: 'useLower', type: 'bool' }, { key: 'useUpper', id: 'useUpper', type: 'bool' }, { key: 'useDigits', id: 'useDigits', type: 'bool' }, { key: 'useSymbols', id: 'useSymbols', type: 'bool' }, { key: 'excludeAmbiguous', id: 'excludeAmbiguous', type: 'bool' }, { key: 'requireEach', id: 'requireEach', type: 'bool' } ]; var STRENGTH_LABELS = ['very weak', 'weak', 'fair', 'strong', 'very strong']; var STRENGTH_THRESHOLDS = [28, 40, 60, 80]; function byId(id) { return document.getElementById(id); } function clampInt(value, fallback, min, max) { var n = parseInt(value, 10); if (isNaN(n)) return fallback; if (n < min) return min; if (n > max) return max; return n; } function loadState() { var state = {}; var key; for (key in DEFAULTS) { if (Object.prototype.hasOwnProperty.call(DEFAULTS, key)) state[key] = DEFAULTS[key]; } try { var raw = localStorage.getItem(STORAGE_KEY); if (raw) { var saved = JSON.parse(raw); if (saved && typeof saved === 'object') { for (key in saved) { if (Object.prototype.hasOwnProperty.call(state, key)) state[key] = saved[key]; } } } } catch (e) {} return state; } function saveState(state) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {} } function readState() { var state = {}; var i; var field; var el; for (i = 0; i < FIELDS.length; i += 1) { field = FIELDS[i]; el = byId(field.id); if (!el) continue; if (field.type === 'bool') { state[field.key] = el.checked; } else if (field.type === 'int') { state[field.key] = clampInt(el.value, DEFAULTS[field.key], field.min, field.max); } else { state[field.key] = el.value; } } var checked = document.querySelector('input[name="mode"]:checked'); state.mode = checked ? checked.value : DEFAULTS.mode; return state; } function applyState(state) { var i; var field; var el; for (i = 0; i < FIELDS.length; i += 1) { field = FIELDS[i]; el = byId(field.id); if (!el) continue; if (field.type === 'bool') { el.checked = Boolean(state[field.key]); } else if (field.type === 'int') { el.value = clampInt(state[field.key], DEFAULTS[field.key], field.min, field.max); } else { el.value = state[field.key] === undefined || state[field.key] === null ? '' : state[field.key]; } } var radios = document.querySelectorAll('input[name="mode"]'); for (i = 0; i < radios.length; i += 1) { radios[i].checked = radios[i].value === state.mode; } } function updatePanels(state) { var memorable = byId('panel-memorable'); var random = byId('panel-random'); if (memorable) memorable.hidden = state.mode !== 'memorable'; if (random) random.hidden = state.mode !== 'random'; } function updateLengthOut() { var length = byId('length'); var out = byId('length-out'); if (length && out) out.textContent = length.value; } function strengthLevel(bits) { var i; for (i = 0; i < STRENGTH_THRESHOLDS.length; i += 1) { if (bits < STRENGTH_THRESHOLDS[i]) return i; } return STRENGTH_LABELS.length - 1; } function buildStrength(bits) { var level = strengthLevel(bits); var wrap = document.createElement('span'); wrap.className = 'strength'; wrap.setAttribute('role', 'img'); wrap.setAttribute('aria-label', 'strength: ' + STRENGTH_LABELS[level]); var i; for (i = 0; i < STRENGTH_LABELS.length; i += 1) { var seg = document.createElement('i'); if (i <= level) seg.className = 'on'; wrap.appendChild(seg); } return wrap; } function showError(message) { var box = byId('error'); if (!box) return; if (message) { box.textContent = message; box.hidden = false; } else { box.textContent = ''; box.hidden = true; } } function copyWithFallback(text) { if (navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard.writeText(text); } return new Promise(function (resolve, reject) { var area = document.createElement('textarea'); area.value = text; area.setAttribute('readonly', 'readonly'); area.style.position = 'absolute'; area.style.left = '-9999px'; document.body.appendChild(area); area.select(); var ok = false; try { ok = document.execCommand('copy'); } catch (e) { ok = false; } document.body.removeChild(area); if (ok) { resolve(); } else { reject(new Error('copy failed')); } }); } function copyPassword(value, button) { var original = button.getAttribute('data-label') || 'Copy'; copyWithFallback(value).then(function () { button.textContent = 'Copied'; }, function () { button.textContent = 'Failed'; }).then(function () { window.setTimeout(function () { button.textContent = original; }, 1200); }); } function renderPasswords(data) { var container = byId('results'); if (!container) return; container.textContent = ''; var list = (data && data.passwords) || []; for (let i = 0; i < list.length; i += 1) { const item = list[i]; const row = document.createElement('div'); row.className = 'pw'; const value = document.createElement('pre'); value.className = 'pw-value'; value.textContent = item.value; const meta = document.createElement('div'); meta.className = 'pw-meta'; const entropy = document.createElement('span'); entropy.className = 'pw-entropy'; var approximate = Boolean(item.entropyApproximate); entropy.textContent = (approximate ? '~' : '') + item.entropy + ' bits'; if (approximate) { entropy.setAttribute('title', 'Upper bound: the generator samples the valid passwords non-uniformly.'); } const level = strengthLevel(item.entropy); const label = document.createElement('span'); label.className = 'pw-strength-label'; label.textContent = STRENGTH_LABELS[level] + (approximate ? ' (approx.)' : ''); const copy = document.createElement('button'); copy.type = 'button'; copy.className = 'button copy'; copy.textContent = 'Copy'; copy.setAttribute('data-label', 'Copy'); copy.setAttribute('aria-label', 'Copy password'); copy.addEventListener('click', function () { copyPassword(item.value, copy); }); meta.appendChild(entropy); meta.appendChild(buildStrength(item.entropy)); meta.appendChild(label); meta.appendChild(copy); row.appendChild(value); row.appendChild(meta); container.appendChild(row); } } function payloadFor(state) { if (state.mode === 'random') { var payload = { mode: 'random', length: state.length, useLower: state.useLower, useUpper: state.useUpper, useDigits: state.useDigits, useSymbols: state.useSymbols, excludeAmbiguous: state.excludeAmbiguous, requireEach: state.requireEach, count: state.count }; // The server rejects invalid symbol sets, so only send the custom set // when the symbols class is actually in use. if (state.useSymbols) payload.customSymbols = state.customSymbols; return payload; } return { mode: 'memorable', words: state.words, digits: state.digits, digitSeparator: state.digitSeparator, separator: state.separator, capitalization: state.capitalization, list: state.list, count: state.count }; } function generate() { var state = readState(); saveState(state); showError(''); fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payloadFor(state)) }).then(function (response) { return response.json().then(function (data) { if (!response.ok) { throw new Error(data && data.error ? data.error : 'request failed with status ' + response.status); } return data; }); }).then(function (data) { renderPasswords(data); }, function (err) { showError(err && err.message ? err.message : 'request failed'); }); } function loadMeta() { fetch('/api/meta', { headers: { Accept: 'application/json' } }).then(function (response) { return response.json(); }).then(function (data) { var lists = (data && data.lists) || []; var i; for (i = 0; i < lists.length; i += 1) { var option = document.querySelector('#list option[value="' + lists[i].id + '"]'); if (option) option.textContent = lists[i].name + ' (' + lists[i].size + ' words)'; } }, function () {}); } function init() { var state = loadState(); applyState(state); updatePanels(state); updateLengthOut(); var i; var field; for (i = 0; i < FIELDS.length; i += 1) { field = FIELDS[i]; (function (f) { var el = byId(f.id); if (!el) return; var handler = function () { var current = readState(); saveState(current); updatePanels(current); updateLengthOut(); }; el.addEventListener('change', handler); el.addEventListener('input', handler); })(field); } var radios = document.querySelectorAll('input[name="mode"]'); for (i = 0; i < radios.length; i += 1) { radios[i].addEventListener('change', function () { var current = readState(); saveState(current); updatePanels(current); }); } var generateBtn = byId('generate'); if (generateBtn) generateBtn.addEventListener('click', generate); var regenerateBtn = byId('regenerate'); if (regenerateBtn) regenerateBtn.addEventListener('click', generate); loadMeta(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();