'use strict';
// ---- Passwords: your Vaultwarden vault. The vault stays end-to-end encrypted; unlocking asks for your master password, which goes to the server's
// Bitwarden tool only to open it for a few idle minutes. Lists never carry passwords: a password, code or note is fetched only when you open that entry.
const vtReq = async (p, body) => {
const r = await fetch('/api/vault' + p, body === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
const j = await r.json().catch(() => ({}));
if (!r.ok) throw Object.assign(new Error(j.error || 'Request failed (' + r.status + ')'), j, { status: r.status });
return j;
};
const vtState = { q: '', folder: '', items: null, folders: [] };
const vtType = { 1: 'Login', 2: 'Note', 3: 'Card', 4: 'Identity' };
let vtClip = 0;
/** Copy something private, and clear the clipboard again after half a minute if it is still ours. */
async function vtCopy(text, what) {
try { await navigator.clipboard.writeText(text); toast(what + ' copied (cleared in 30 seconds)'); clearTimeout(vtClip); vtClip = setTimeout(async () => { try { if ((await navigator.clipboard.readText()) === text) await navigator.clipboard.writeText(''); } catch { try { await navigator.clipboard.writeText(''); } catch {} } }, 30000); }
catch { toast('Could not copy'); }
}
const vtHost = u => { try { return new URL(/^[a-z]+:\/\//i.test(u) ? u : 'https://' + u).hostname.replace(/^www\./, ''); } catch { return ''; } };
const vtSafeUrl = u => { try { const x = new URL(/^[a-z]+:\/\//i.test(u) ? u : 'https://' + u); return /^https?:$/.test(x.protocol) ? x.href : ''; } catch { return ''; } };
async function viewVault() {
const v = $('#view'); v.innerHTML = '
';
let st; try { st = await vtReq('/status'); } catch (e) { v.innerHTML = `Passwords
${esc(e.message)}
`; return; }
if (!location.hash.startsWith('#/vault')) return;
st.unlocked ? vtList(st) : vtUnlock(st);
}
function vtUnlock(st, need2fa) {
const v = $('#view'), host = vtHost((typeof cfg !== 'undefined' && cfg.vault) || st.url);
v.innerHTML = ``;
const f = $('#vtf'); setTimeout(() => (f.querySelector(need2fa ? '#vtc' : st.loggedIn ? '#vtp' : '#vte') || {}).focus?.(), 0);
if ($('#vtout')) $('#vtout').onclick = () => confirmSheet('Use a different account?', 'This signs the hub out of the vault. Nothing is deleted from the vault itself.', 'Sign out of vault', async () => { try { await vtReq('/forget', {}); viewVault(); } catch (e) { toast(e.message); } });
f.onsubmit = async e => {
e.preventDefault(); $('#vterr').textContent = ''; $('#vtgo').disabled = true;
try { const body = { password: $('#vtp').value }; if ($('#vte')) body.email = $('#vte').value; if ($('#vtc')) body.code = $('#vtc').value; const r = await vtReq('/unlock', body); $('#vtp').value = ''; vtState.items = null; vtList(r); }
catch (err) { if (err.need2fa) return vtUnlock({ ...st, email: st.email || ($('#vte') && $('#vte').value) }, true) || void 0; $('#vterr').textContent = err.message; $('#vtgo').disabled = false; }
};
}
async function vtList(st) {
const v = $('#view');
v.innerHTML = `Passwords
Locks itself after a few idle minutes. Open the full vault to add or change entries.
`;
const lockedOut = () => { vtState.items = null; vtUnlock({ ...st, unlocked: false }); };
$('#vtlock').onclick = async () => { try { await vtReq('/lock', {}); } catch {} lockedOut(); toast('Vault locked'); };
$('#vtsync').onclick = async () => { try { await vtReq('/sync', {}); await load(); toast('Refreshed'); } catch (e) { e.locked ? lockedOut() : toast(e.message); } };
let timer; $('#vtq').oninput = e => { vtState.q = e.target.value; clearTimeout(timer); timer = setTimeout(load, 250); };
async function load() {
try { const r = await vtReq('/items?q=' + encodeURIComponent(vtState.q.trim())); if (!location.hash.startsWith('#/vault')) return; vtState.items = r.items; vtState.folders = r.folders; draw(); }
catch (e) { if (e.locked) return lockedOut(); $('#vtl').innerHTML = `${esc(e.message)}
`; }
}
function draw() {
const fo = vtState.folders; $('#vtfo').innerHTML = fo.length ? `` : '';
$('#vtfo').querySelectorAll('a').forEach(a => a.onclick = e => { e.preventDefault(); vtState.folder = a.dataset.f; draw(); });
const rows = vtState.items.filter(i => !vtState.folder || i.folderId === vtState.folder);
$('#vtl').innerHTML = rows.length ? `${rows.map(i => ``).join('')}
` : `${vtState.q ? 'Nothing matches.' : 'Your vault is empty.'}
`;
$('#vtl').querySelectorAll('.row').forEach(b => b.onclick = () => vtOpen(b.dataset.id, lockedOut));
}
if (vtState.items) draw(); load();
}
async function vtOpen(id, lockedOut) {
let it; try { it = await vtReq('/item/' + encodeURIComponent(id)); } catch (e) { return e.locked ? lockedOut() : toast(e.message); }
const field = (label, value, key, secret) => value ? `${esc(label)}${secret ? '••••••••••' : esc(value)}${secret ? `` : ''}
` : '';
const vals = { user: it.username, pass: it.password, totp: it.totp };
const extra = []; if (it.card) { vals.cnum = it.card.number; vals.ccode = it.card.code; }
(it.fields || []).forEach((f, n) => { vals['f' + n] = f.value; });
const links = it.uris.map(vtSafeUrl).filter(Boolean);
const el = openSheet(it.name || 'Entry', vtType[it.type] || '', `${field('Username', it.username, 'user')}${field('Password', it.password, 'pass', true)}${field('One-time code', it.totp, 'totp')}
${it.card ? field('Cardholder', it.card.holder, 'chold') + field('Card number', it.card.number, 'cnum', true) + field('Security code', it.card.code, 'ccode', true) + field('Expires', [it.card.expMonth, it.card.expYear].filter(Boolean).join('/'), 'cexp') : ''}
${(it.fields || []).map((f, n) => field(f.name || 'Field', f.value, 'f' + n, f.hidden)).join('')}
${links.map(u => ``).join('')}
${it.notes ? `` : ''}`);
Object.assign(vals, { chold: it.card && it.card.holder, cexp: it.card && [it.card.expMonth, it.card.expYear].filter(Boolean).join('/') });
el.querySelectorAll('.vtcp').forEach(b => b.onclick = () => vtCopy(vals[b.dataset.k], b.dataset.w));
el.querySelectorAll('.vtshow').forEach(b => b.onclick = () => { const s = el.querySelector(`.vtv[data-k="${b.dataset.k}"]`), on = b.textContent === 'Show'; s.textContent = on ? vals[b.dataset.k] : '••••••••••'; b.textContent = on ? 'Hide' : 'Show'; });
}