@dotrino/vaultd 0.126.0 → 0.127.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/src/admin.js +10 -0
- package/package.json +2 -2
- package/src/approvals.js +9 -4
- package/src/vault.js +105 -33
package/lib/src/admin.js
CHANGED
|
@@ -284,6 +284,12 @@ export function createAdminDesk ({
|
|
|
284
284
|
by
|
|
285
285
|
})
|
|
286
286
|
const keys = result?.keys || []
|
|
287
|
+
// EN ESPERA DE APROBACIÓN: todavía no cambió nada, así que no hay de qué avisar. El
|
|
288
|
+
// aviso sale cuando se aprueba y se escribe de verdad.
|
|
289
|
+
if (result?.pending) {
|
|
290
|
+
audit('admin.var.pending', { by, ns: toScope ? data.ns : null, id: result.pending, count: data.items.length })
|
|
291
|
+
return { ok: true, result }
|
|
292
|
+
}
|
|
287
293
|
audit('admin.var.set', { by, ns: toScope ? data.ns : null, device: toDevice ? await deviceIdOf(data.pub).catch(() => null) : null, keys })
|
|
288
294
|
await notify('vars', { by, keys, ns: toScope ? data.ns : null })
|
|
289
295
|
return { ok: true, result: result || { ok: true } }
|
|
@@ -308,6 +314,10 @@ export function createAdminDesk ({
|
|
|
308
314
|
caller: chk.device,
|
|
309
315
|
by
|
|
310
316
|
})
|
|
317
|
+
if (result?.pending) {
|
|
318
|
+
audit('admin.var.pending', { by, ns: toScope ? data.ns : null, id: result.pending, count: 1 })
|
|
319
|
+
return { ok: true, result }
|
|
320
|
+
}
|
|
311
321
|
audit('admin.var.set', { by, ns: toScope ? data.ns : null, device: toDevice ? await deviceIdOf(data.pub).catch(() => null) : null, key: data.key })
|
|
312
322
|
// Cambiar la configuración de un servicio a distancia no puede ser invisible: es
|
|
313
323
|
// la contrapartida de delegar (F3 de docs/consola-remota.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.127.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Certificador personal de Dotrino: daemon headless que custodia la clave maestra y delega capacidades a tus dispositivos por el proxy. Tu CA propia.",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@dotrino/identity": "0.101.0",
|
|
26
26
|
"@dotrino/opaque": "0.1.0",
|
|
27
27
|
"@dotrino/passmanager": "0.17.0",
|
|
28
|
-
"@dotrino/proxy-client": "0.
|
|
28
|
+
"@dotrino/proxy-client": "0.24.0",
|
|
29
29
|
"@dotrino/roadmap": "0.3.7",
|
|
30
30
|
"@dotrino/store": "0.11.0",
|
|
31
31
|
"@dotrino/update": "0.1.1",
|
package/src/approvals.js
CHANGED
|
@@ -73,7 +73,9 @@ export function createApprovals ({ now = Date.now, pendingTtlMs = PENDING_TTL_MS
|
|
|
73
73
|
* de un sobre (`handleApproval`). Si mañana aparece otro sitio que publique esta lista,
|
|
74
74
|
* esto es lo que hay que recordar.
|
|
75
75
|
*/
|
|
76
|
-
|
|
76
|
+
// `kind` dice QUÉ se pide: `read` (llevarse las claves, lo de siempre) o `write` (guardar
|
|
77
|
+
// variables). La pantalla del teléfono no puede decir «pide tus claves» a una escritura.
|
|
78
|
+
const publicOf = (p) => ({ id: p.id, ns: p.ns, kind: p.kind, deviceId: p.deviceId, label: p.label, ts: p.ts, exp: p.exp, ctx: p.ctx || null })
|
|
77
79
|
|
|
78
80
|
return {
|
|
79
81
|
/** Apunta un pedido. Uno por cajón y aparato: pedir otra vez reemplaza al anterior. */
|
|
@@ -82,12 +84,15 @@ export function createApprovals ({ now = Date.now, pendingTtlMs = PENDING_TTL_MS
|
|
|
82
84
|
* apruebe— y tiene que volver por el mismo sitio: desde que la bóveda atiende también
|
|
83
85
|
* por un socket local, mandarla siempre por el proxio la dejaba en el vacío.
|
|
84
86
|
*/
|
|
85
|
-
request ({ ns, device, deviceId, label = '', ek, ctx = null, from = null, ttlMs = null }) {
|
|
86
|
-
|
|
87
|
+
request ({ ns, device, deviceId, label = '', ek, ctx = null, from = null, ttlMs = null, kind = 'read' }) {
|
|
88
|
+
// Pedir las claves otra vez reemplaza al pedido anterior: es la misma pregunta. Una
|
|
89
|
+
// ESCRITURA no: cada una lleva su propio valor, y reemplazarla tiraría la anterior
|
|
90
|
+
// sin que nadie dijera que no.
|
|
91
|
+
if (kind !== 'write') for (const [id, p] of pending) if (p.kind !== 'write' && p.ns === ns && p.device === device) pending.delete(id)
|
|
87
92
|
const ts = now()
|
|
88
93
|
// `ttlMs` lo pone quien pide: una actualización aguanta un día y un comando cinco
|
|
89
94
|
// minutos, y la mesa no tiene por qué saber cuál es cuál (ver `PENDING_TTL_MS`).
|
|
90
|
-
const p = { id: rnd(), ns, device, deviceId, label, ek, ctx, ts, exp: ts + (ttlMs || pendingTtlMs), from }
|
|
95
|
+
const p = { id: rnd(), ns, kind, device, deviceId, label, ek, ctx, ts, exp: ts + (ttlMs || pendingTtlMs), from }
|
|
91
96
|
pending.set(p.id, p)
|
|
92
97
|
return publicOf(p)
|
|
93
98
|
},
|
package/src/vault.js
CHANGED
|
@@ -155,6 +155,31 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
155
155
|
return !Acta.memberCan(record, pub, 'unattended')
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* APUNTA UN PEDIDO QUE NO ENTREGA UN CAJÓN, avisa a quien aprueba y deja la respuesta en
|
|
160
|
+
* `onAnswer(sí|no)`. Lo usan la bóveda de contraseñas (un aparato espera su sí) y guardar
|
|
161
|
+
* variables (la escritura se aplica al aprobar). Si nadie contesta, vence solo con un no:
|
|
162
|
+
* nada se queda colgado para siempre.
|
|
163
|
+
*/
|
|
164
|
+
async function awaitApproval ({ device, ns, kind, what, op = null, ctx = null, onAnswer }) {
|
|
165
|
+
const deviceId = await deviceIdOf(device).catch(() => null)
|
|
166
|
+
const record = await refreshActa()
|
|
167
|
+
const label = (record?.members || []).find((m) => m.pub === device)?.label || ''
|
|
168
|
+
const pend = approvals.request({ ns, device, deviceId, label, ek: '', ctx, kind })
|
|
169
|
+
audit(`${what}.pending`, { device: deviceId, ns, id: pend.id, op })
|
|
170
|
+
log(`[vault] ${what}: ${deviceId || '????-????'} is waiting for approval (${pend.id})`)
|
|
171
|
+
waiters.set(pend.id, onAnswer)
|
|
172
|
+
const t = setTimeout(() => {
|
|
173
|
+
if (waiters.delete(pend.id)) {
|
|
174
|
+
audit(`${what}.expired`, { device: deviceId, ns, id: pend.id })
|
|
175
|
+
onAnswer(false)
|
|
176
|
+
}
|
|
177
|
+
}, PENDING_TTL_MS)
|
|
178
|
+
t.unref?.()
|
|
179
|
+
await notifyApprovers(pend, record)
|
|
180
|
+
return pend
|
|
181
|
+
}
|
|
182
|
+
|
|
158
183
|
/**
|
|
159
184
|
* QUÉ COMANDO ESTÁ PIDIENDO LAS CLAVES, Y DESDE QUÉ CARPETA — medido, no creído.
|
|
160
185
|
*
|
|
@@ -1595,16 +1620,23 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1595
1620
|
const id = typeof p.data?.id === 'string' ? p.data.id : ''
|
|
1596
1621
|
const pend = approvals.take(id)
|
|
1597
1622
|
if (!pend) return reply(from, { type: MSG.ERROR, error: 'approval: unknown or expired request' })
|
|
1598
|
-
// PEDIDOS QUE NO
|
|
1599
|
-
// de tocar `resultFor`, que asume un cajón y una `ek`: aquí no hay nada
|
|
1600
|
-
// solo una promesa esperando un sí o un no.
|
|
1623
|
+
// PEDIDOS QUE NO ENTREGAN UN CAJÓN (la bóveda de contraseñas, guardar variables). Se
|
|
1624
|
+
// resuelven ANTES de tocar `resultFor`, que asume un cajón y una `ek`: aquí no hay nada
|
|
1625
|
+
// que sellar, solo una promesa esperando un sí o un no.
|
|
1601
1626
|
if (waiters.has(id)) {
|
|
1602
1627
|
const resolver = waiters.get(id)
|
|
1603
1628
|
waiters.delete(id)
|
|
1604
1629
|
const ok = op === 'approve'
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1630
|
+
const what = pend.kind === 'write' ? 'vars' : 'passwords'
|
|
1631
|
+
audit(ok ? `${what}.approved` : `${what}.denied`, { device: pend.deviceId, ns: pend.ns, id, by })
|
|
1632
|
+
log(`[vault] ${what}: request ${pend.id} of ${pend.deviceId} ${ok ? 'approved' : 'DENIED'} by ${by}`)
|
|
1633
|
+
// Lo aprobado SE HACE AQUÍ (una escritura se aplica ahora), y si falla quien aprobó
|
|
1634
|
+
// tiene que enterarse: decirle «hecho» a algo que no se guardó es el peor resultado.
|
|
1635
|
+
try { await resolver(ok) } catch (e) {
|
|
1636
|
+
log(`[vault] ${what}: ${pend.id} was approved but could not be applied: ${e.message}`)
|
|
1637
|
+
audit(`${what}.apply-failed`, { id, reason: e.message })
|
|
1638
|
+
return reply(from, { type: MSG.ERROR, error: `approval: approved, but it could not be applied: ${e.message}` })
|
|
1639
|
+
}
|
|
1608
1640
|
return answer({ op: `${op}.result`, id, ok: true })
|
|
1609
1641
|
}
|
|
1610
1642
|
if (op === 'deny') {
|
|
@@ -1917,24 +1949,9 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1917
1949
|
// El teléfono: se apunta el pedido, se le avisa y esta promesa espera su firma.
|
|
1918
1950
|
// Es el mismo camino que ya recorren los cajones de secretos.
|
|
1919
1951
|
approve: async ({ pubkey, op }) => {
|
|
1920
|
-
|
|
1921
|
-
const
|
|
1922
|
-
|
|
1923
|
-
audit('passwords.pending', { device: deviceId, id: pend.id, op })
|
|
1924
|
-
log(`[vault] passwords: ${deviceId || '????-????'} is waiting for approval (${pend.id})`)
|
|
1925
|
-
const espera = new Promise((resolve) => {
|
|
1926
|
-
waiters.set(pend.id, resolve)
|
|
1927
|
-
// Si nadie contesta, vence solo: la promesa no se queda colgada para siempre
|
|
1928
|
-
// y el aparato recibe un no en vez de esperar sin fin.
|
|
1929
|
-
const t = setTimeout(() => {
|
|
1930
|
-
if (waiters.delete(pend.id)) {
|
|
1931
|
-
audit('passwords.expired', { device: deviceId, id: pend.id })
|
|
1932
|
-
resolve(false)
|
|
1933
|
-
}
|
|
1934
|
-
}, PENDING_TTL_MS)
|
|
1935
|
-
t.unref?.()
|
|
1936
|
-
})
|
|
1937
|
-
await notifyApprovers(pend, actaCache)
|
|
1952
|
+
let resolve
|
|
1953
|
+
const espera = new Promise((r) => { resolve = r })
|
|
1954
|
+
await awaitApproval({ device: pubkey, ns: 'passwords', kind: 'read', what: 'passwords', op, onAnswer: resolve })
|
|
1938
1955
|
return espera
|
|
1939
1956
|
},
|
|
1940
1957
|
audit,
|
|
@@ -2111,7 +2128,11 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2111
2128
|
// El fallo NO se traga. Que un aviso no salga es exactamente lo que deja a dos
|
|
2112
2129
|
// bóvedas con actas distintas sin que nadie se entere, y encontrarlo sin una línea
|
|
2113
2130
|
// de log cuesta días (la última vez, tres).
|
|
2114
|
-
|
|
2131
|
+
// CALLADO (`quiet`): se encola igual, pero no toca el timbre push. Un aviso de que
|
|
2132
|
+
// el perfil cambió puede esperar a que el aparato abra solo; timbrar por él hacía
|
|
2133
|
+
// sonar el teléfono con «alguien pide tus claves» sin ningún pedido detrás.
|
|
2134
|
+
// El timbre es para los pedidos (`notifyApprovers`), que sí esperan una mano.
|
|
2135
|
+
try { client.sendByPubkey(pub, { type: MSG.ADMIN_EVENT, body, signature, acta }, { quiet: true }) }
|
|
2115
2136
|
catch (e) { log(`[vault] could not notify ${pub.slice(0, 24)}… of "${ev}": ${e.message}`) }
|
|
2116
2137
|
}
|
|
2117
2138
|
for (const m of miembros) avisar(m.pub)
|
|
@@ -2300,11 +2321,20 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2300
2321
|
// PÚBLICA Y PRIVADA VAN IGUAL (dueño, 2026-09-02: «la única diferencia es si se
|
|
2301
2322
|
// despachan o no, son políticas; dales el mismo tratamiento de seguridad»). La
|
|
2302
2323
|
// marca solo decide si se entrega sin aprobación.
|
|
2324
|
+
const apply = async () => {
|
|
2325
|
+
await checkAuthor(owner, key, sealed, caller)
|
|
2326
|
+
await checkRecipients(owner, sealed.wraps, !!isPublic)
|
|
2327
|
+
await secrets.putSealed(owner, key, sealed, { by: who, public: isPublic })
|
|
2328
|
+
audit('secret.set', { ns: ns || null, key, sealed: true }); scheduleNotice(ns)
|
|
2329
|
+
await settleDebts(owner)
|
|
2330
|
+
}
|
|
2331
|
+
// Comprobado ANTES de dejarlo en espera: un sobre malo falla al guardarlo, no cinco
|
|
2332
|
+
// minutos después en la pantalla de quien aprueba. `apply` lo repite al aprobar.
|
|
2303
2333
|
await checkAuthor(owner, key, sealed, caller)
|
|
2304
2334
|
await checkRecipients(owner, sealed.wraps, !!isPublic)
|
|
2305
|
-
await
|
|
2306
|
-
|
|
2307
|
-
await
|
|
2335
|
+
const pending = await holdWrite({ caller, ns, pub, keys: [key], apply })
|
|
2336
|
+
if (pending) return { ok: true, key, pending }
|
|
2337
|
+
await apply()
|
|
2308
2338
|
return { ok: true, key }
|
|
2309
2339
|
}
|
|
2310
2340
|
// NO HAY OTRO CAMINO (dueño, 2026-09-02: «no dejes caminos viejos ni fallbacks; no hay
|
|
@@ -2345,16 +2375,58 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2345
2375
|
}
|
|
2346
2376
|
// Se comprueban TODAS antes de escribir NINGUNA: media carga aplicada es una
|
|
2347
2377
|
// configuración que nadie quiso, y el servicio se reinicia con ella.
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2378
|
+
const apply = async () => {
|
|
2379
|
+
for (const it of items) {
|
|
2380
|
+
await checkAuthor(owner, it.key, it.sealed, caller)
|
|
2381
|
+
await checkRecipients(owner, it.sealed.wraps, !!it.public)
|
|
2382
|
+
}
|
|
2383
|
+
for (const it of items) {
|
|
2384
|
+
await secrets.putSealed(owner, it.key, it.sealed, { by: who, public: !!it.public })
|
|
2385
|
+
keys.push(it.key)
|
|
2386
|
+
}
|
|
2387
|
+
audit('secret.setMany', { ns: ns || null, keys }); scheduleNotice(ns)
|
|
2388
|
+
await settleDebts(owner)
|
|
2351
2389
|
}
|
|
2352
|
-
|
|
2353
|
-
|
|
2390
|
+
const pending = await holdWrite({ caller, ns, pub, keys: items.map((it) => it.key), apply })
|
|
2391
|
+
if (pending) return { ok: true, keys: [], pending }
|
|
2392
|
+
await apply()
|
|
2354
2393
|
return { ok: true, keys }
|
|
2355
2394
|
}
|
|
2356
2395
|
}
|
|
2357
2396
|
|
|
2397
|
+
/**
|
|
2398
|
+
* GUARDAR VARIABLES TAMBIÉN PIDE PERMISO (dueño, 2026-09-23: «si el admin no es
|
|
2399
|
+
* unattended, sí debería pedir permiso para almacenar variables»).
|
|
2400
|
+
*
|
|
2401
|
+
* Es el mismo permiso que ya decide si un aparato se lleva las claves solo: quien tiene
|
|
2402
|
+
* `unattended` escribe sin preguntar; quien no, deja la escritura en espera y el teléfono
|
|
2403
|
+
* decide. Un permiso, una regla, para leer y para escribir.
|
|
2404
|
+
*
|
|
2405
|
+
* No se espera DENTRO de la llamada: la consola corta a los 20 s y aprobar puede tardar
|
|
2406
|
+
* cinco minutos. Se contesta «pendiente» en el acto, el sobre se queda en memoria (va
|
|
2407
|
+
* sellado, la bóveda no lo abre) y se escribe al aprobar. Denegado o vencido, se tira.
|
|
2408
|
+
*
|
|
2409
|
+
* Devuelve el id del pedido si la escritura quedó en espera, o `null` si se puede hacer ya.
|
|
2410
|
+
*/
|
|
2411
|
+
async function holdWrite ({ caller, ns, pub, keys, apply }) {
|
|
2412
|
+
if (!needsApproval(caller, await refreshActa())) return null
|
|
2413
|
+
// Quien llama ya comprobó el sobre; `apply` lo vuelve a comprobar al aprobar, porque el
|
|
2414
|
+
// acta pudo cambiar en medio.
|
|
2415
|
+
const target = ns || await deviceIdOf(pub).catch(() => null) || '?'
|
|
2416
|
+
const pend = await awaitApproval({
|
|
2417
|
+
device: caller, ns: target, kind: 'write', what: 'vars',
|
|
2418
|
+
// LOS NOMBRES van en el contexto, que viaja SELLADO al que aprueba: el proxio no
|
|
2419
|
+
// tiene por qué aprender cómo se llama la configuración de un servicio.
|
|
2420
|
+
ctx: { keys },
|
|
2421
|
+
onAnswer: async (ok) => {
|
|
2422
|
+
if (!ok) return
|
|
2423
|
+
await apply()
|
|
2424
|
+
await notifyMembers('vars', { by: await deviceIdOf(caller).catch(() => null), keys, ns: ns || null })
|
|
2425
|
+
}
|
|
2426
|
+
})
|
|
2427
|
+
return pend.id
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2358
2430
|
const admin = createAdminDesk({
|
|
2359
2431
|
desk,
|
|
2360
2432
|
deviceIdOf,
|