Passwords: open entries instantly from an in-memory copy; one-time code loads separately
build-container / build (push) Successful in 6m17s
build-container / build (push) Successful in 6m17s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4752aca43c
commit
5f5d5ae214
2 files changed
+12
-10
No files matched your search
+9
-8
@@ -13,6 +13,7 @@ const enabled = () => !!URL_;
|
||||
|
||||
const sessions = new Map(); // user id -> { key, last }
|
||||
const chains = new Map(); // user id -> the last bw call, so one person's calls run one at a time
|
||||
const cache = new Map(); // user id -> Map(item id -> item) from the last list, so opening an entry needs no new bw call; dropped on lock, idle and any change
|
||||
const fails = new Map(); // user id -> { n, until }, slows down password guessing
|
||||
|
||||
const json = (res, code, o) => { res.writeHead(code, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(o)); };
|
||||
@@ -38,7 +39,7 @@ function bw(uid, args, { session, password, input, timeout = 45e3 } = {}) {
|
||||
}
|
||||
const parse = s => { try { return JSON.parse(s); } catch { throw fail(502, 'The vault gave an unexpected answer'); } };
|
||||
|
||||
const lock = uid => { const s = sessions.get(uid); sessions.delete(uid); if (s) bw(uid, ['lock']).catch(() => {}); };
|
||||
const lock = uid => { const s = sessions.get(uid); sessions.delete(uid); cache.delete(uid); if (s) bw(uid, ['lock']).catch(() => {}); };
|
||||
setInterval(() => { const now = Date.now(); for (const [uid, s] of sessions) if (now - s.last > IDLE_MS) lock(uid); }, 30e3).unref();
|
||||
|
||||
const keyOf = uid => { const s = sessions.get(uid); if (!s) throw fail(401, 'The vault is locked', { locked: true }); s.last = Date.now(); return s.key; };
|
||||
@@ -76,7 +77,7 @@ async function saveItem(uid, b, key) {
|
||||
const l = item.login = item.login || {}; l.username = clip(b.username, 1000) || null; l.password = clip(b.password, 1000) || null; l.totp = clip(b.totp, 1000).trim() || null;
|
||||
const old = l.uris || []; l.uris = uris.map(u => ({ ...(old.find(o => o.uri === u) || { match: null }), uri: u }));
|
||||
}
|
||||
const out = parse(await bw(uid, b.id ? ['edit', 'item', b.id] : ['create', 'item'], { session: key, input: enc(item) }));
|
||||
const out = parse(await bw(uid, b.id ? ['edit', 'item', b.id] : ['create', 'item'], { session: key, input: enc(item) })); cache.delete(uid);
|
||||
bw(uid, ['sync'], { session: key }).catch(() => {});
|
||||
return { id: out.id };
|
||||
}
|
||||
@@ -131,9 +132,9 @@ async function handle(req, res) {
|
||||
if (p === '/unlock' && req.method === 'POST') return json(res, 200, await unlock(uid, JSON.parse(await readBody(req, 1e4) || '{}')));
|
||||
if (p === '/lock' && req.method === 'POST') { lock(uid); return json(res, 200, { ok: true }); }
|
||||
if (p === '/forget' && req.method === 'POST') { lock(uid); await bw(uid, ['logout']).catch(() => {}); return json(res, 200, { ok: true }); } // sign this account out of the hub completely
|
||||
if (p === '/sync' && req.method === 'POST') { try { await bw(uid, ['sync'], { session: keyOf(uid) }); } catch (e) { throw asLocked(e); } return json(res, 200, { ok: true }); }
|
||||
if (p === '/sync' && req.method === 'POST') { try { await bw(uid, ['sync'], { session: keyOf(uid) }); cache.delete(uid); } catch (e) { throw asLocked(e); } return json(res, 200, { ok: true }); }
|
||||
if (p === '/item' && req.method === 'POST') { const key = keyOf(uid); try { return json(res, 200, await saveItem(uid, JSON.parse(await readBody(req, 5e4) || '{}'), key)); } catch (e) { throw asLocked(e); } }
|
||||
if (p === '/item/delete' && req.method === 'POST') { const { id } = JSON.parse(await readBody(req, 1e4) || '{}'); if (!ID.test(id || '')) throw fail(400, 'Bad item'); const key = keyOf(uid); try { await bw(uid, ['delete', 'item', id], { session: key }); } catch (e) { throw asLocked(e); } bw(uid, ['sync'], { session: key }).catch(() => {}); return json(res, 200, { ok: true }); } // goes to the vault's trash, where it can still be restored
|
||||
if (p === '/item/delete' && req.method === 'POST') { const { id } = JSON.parse(await readBody(req, 1e4) || '{}'); if (!ID.test(id || '')) throw fail(400, 'Bad item'); const key = keyOf(uid); try { await bw(uid, ['delete', 'item', id], { session: key }); cache.delete(uid); } catch (e) { throw asLocked(e); } bw(uid, ['sync'], { session: key }).catch(() => {}); return json(res, 200, { ok: true }); } // goes to the vault's trash, where it can still be restored
|
||||
if (p === '/generate' && req.method === 'GET') return json(res, 200, await generate(uid, q));
|
||||
if (p === '/sends' && req.method === 'GET') { try { return json(res, 200, { sends: parse(await bw(uid, ['send', 'list'], { session: keyOf(uid) })).filter(x => x.type === 0).map(sendBrief) }); } catch (e) { throw asLocked(e); } }
|
||||
if (p === '/sends' && req.method === 'POST') { const key = keyOf(uid); try { return json(res, 200, await createSend(uid, JSON.parse(await readBody(req, 5e4) || '{}'), key)); } catch (e) { throw asLocked(e); } }
|
||||
@@ -142,19 +143,19 @@ async function handle(req, res) {
|
||||
const key = keyOf(uid), search = String(q.get('q') || '').slice(0, 100).trim(), args = ['list', 'items'];
|
||||
if (search) args.push('--search=' + search);
|
||||
try { const items = parse(await bw(uid, args, { session: key })).filter(i => i.type === 1 || i.type === 2 || i.type === 3 || i.type === 4); // login, note, card, identity
|
||||
const cm = cache.get(uid) || new Map(); for (const i of items) cm.set(i.id, i); cache.set(uid, cm);
|
||||
let folders = []; try { folders = parse(await bw(uid, ['list', 'folders'], { session: key })).filter(f => f.id).map(f => ({ id: f.id, name: f.name })); } catch {}
|
||||
return json(res, 200, { items: items.map(brief).sort((a, b) => (b.favorite - a.favorite) || a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })).slice(0, 1000), folders }); }
|
||||
catch (e) { throw asLocked(e); }
|
||||
}
|
||||
let m;
|
||||
if ((m = /^\/totp\/([^/]+)$/.exec(p)) && req.method === 'GET') { if (!ID.test(m[1])) throw fail(400, 'Bad item'); try { return json(res, 200, { code: (await bw(uid, ['get', 'totp', m[1]], { session: keyOf(uid) })).trim() }); } catch (e) { throw asLocked(e); } }
|
||||
if ((m = /^\/item\/([^/]+)$/.exec(p)) && req.method === 'GET') { // one entry in full, only when a person opens it
|
||||
if (!ID.test(m[1])) throw fail(400, 'Bad item');
|
||||
const key = keyOf(uid);
|
||||
try {
|
||||
const i = parse(await bw(uid, ['get', 'item', m[1]], { session: key })), l = i.login || {};
|
||||
let totp = ''; if (l.totp) { try { totp = (await bw(uid, ['get', 'totp', m[1]], { session: key })).trim(); } catch {} }
|
||||
const c = i.card || {};
|
||||
return json(res, 200, { id: i.id, name: i.name || '', type: i.type, notes: i.notes || '', username: l.username || '', password: l.password || '', totp, totpSeed: l.totp || '', folderId: i.folderId || '', favorite: !!i.favorite,
|
||||
const i = (cache.get(uid) || new Map()).get(m[1]) || parse(await bw(uid, ['get', 'item', m[1]], { session: key })), l = i.login || {}, c = i.card || {};
|
||||
return json(res, 200, { id: i.id, name: i.name || '', type: i.type, notes: i.notes || '', username: l.username || '', password: l.password || '', hasTotp: !!l.totp, totpSeed: l.totp || '', folderId: i.folderId || '', favorite: !!i.favorite,
|
||||
uris: (l.uris || []).map(x => x.uri).filter(Boolean),
|
||||
card: i.type === 3 ? { holder: c.cardholderName || '', brand: c.brand || '', number: c.number || '', code: c.code || '', expMonth: c.expMonth || '', expYear: c.expYear || '' } : null,
|
||||
fields: (i.fields || []).filter(f => f.type !== 3).map(f => ({ name: f.name || '', value: f.value || '', hidden: f.type === 1 })) });
|
||||
|
||||
Reference in new issue
Block a user