@dotrino/vaultd 0.59.0 → 0.61.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/package.json +1 -1
- package/src/daemon.js +5 -0
- package/src/tui/app.js +47 -3
- package/src/tui/i18n.js +18 -4
- package/src/vaultControl.js +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.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": {
|
package/src/daemon.js
CHANGED
|
@@ -340,6 +340,11 @@ export async function runDaemon () {
|
|
|
340
340
|
finally { wipe(ak) }
|
|
341
341
|
return { done: 'perfil desbloqueado' + note, autoLockMs: mgr.profiles.autoLockMs }
|
|
342
342
|
}
|
|
343
|
+
// TECLEAR EN LA PANTALLA DE CONTROL CUENTA COMO USO. Sin esto, navegar por ella no
|
|
344
|
+
// tocaba el daemon —solo lo hacían las acciones—, así que el candado se cerraba a
|
|
345
|
+
// los 5 minutos de ABRIRLO aunque estuvieras delante trabajando. `ref()` ya estira
|
|
346
|
+
// el plazo; esta op no hace nada más, y por eso no devuelve ningún volcado.
|
|
347
|
+
case 'touch': { ref(); return { done: 'ok' } }
|
|
343
348
|
case 'lock': { mgr.profiles.lock(ref()); return { done: 'perfil bloqueado' } }
|
|
344
349
|
// PONER contraseña: los secretos pasan de abrirse con la llave de la máquina a
|
|
345
350
|
// abrirse con la frase. Hay que volver a cerrar la copia maestra con la nueva, o
|
package/src/tui/app.js
CHANGED
|
@@ -127,6 +127,10 @@ function mergeMembersAndCerts (members, issued) {
|
|
|
127
127
|
label: m.label || cert?.label || '',
|
|
128
128
|
isMaster: !!m.isMaster,
|
|
129
129
|
cn: m.cn || null,
|
|
130
|
+
// CUÁNDO ENTRÓ. Venía en el acta y se perdía aquí, así que la lista no podía
|
|
131
|
+
// enseñarlo por mucho que se quisiera. El nombre lo pone el propio aparato y muchas
|
|
132
|
+
// veces no distingue nada: la fecha es lo que deja reconocer cuál es cuál.
|
|
133
|
+
addedAt: m.addedAt || null,
|
|
130
134
|
// El master es la propia bóveda: no tiene (ni necesita) certificado. Un servicio
|
|
131
135
|
// tampoco lleva uno de dispositivo. Marcarlos «sin acceso» sería una alarma falsa.
|
|
132
136
|
noAccess: !cert && !m.isMaster && !m.cn
|
|
@@ -260,6 +264,7 @@ function deviceRows (st, t) {
|
|
|
260
264
|
const label = d.label || t.muted(i.noLabel)
|
|
261
265
|
const vars = devVarsOf(st, d.sub).length
|
|
262
266
|
const debt = debtOf(st, d.sub)
|
|
267
|
+
const desde = d.addedAt ? t.muted(` ${i.deviceSince}:${fmtExp(d.addedAt)}`) : ''
|
|
263
268
|
const extra = (d.certCount > 1 ? t.muted(` certs:${d.certCount}`) : '') +
|
|
264
269
|
(vars ? t.muted(` vars:${vars}`) : '') +
|
|
265
270
|
// EN DEUDA: en el acta y sin poder abrir lo suyo. Va en color de aviso al lado de
|
|
@@ -272,7 +277,7 @@ function deviceRows (st, t) {
|
|
|
272
277
|
: d.isMaster
|
|
273
278
|
? t.muted(i.thisVault)
|
|
274
279
|
: t.muted('scope:' + shortScope(d.scope)) + ' ' + t.muted('exp:' + fmtExp(d.exp))
|
|
275
|
-
rows.push({ text: ` ${t.bold(d.deviceId)} ${label} ${status}${extra}`, sel: true, meta: d })
|
|
280
|
+
rows.push({ text: ` ${t.bold(d.deviceId)} ${label}${desde} ${status}${extra}`, sel: true, meta: d })
|
|
276
281
|
}
|
|
277
282
|
const revoked = st.devices?.revoked || []
|
|
278
283
|
if (revoked.length) {
|
|
@@ -318,7 +323,17 @@ function pairModeRows (st, t) {
|
|
|
318
323
|
* una marca de si los tiene. El de administrar va aparte y avisado: es el único que deja
|
|
319
324
|
* a ese aparato meter y sacar dispositivos sin venir aquí.
|
|
320
325
|
*/
|
|
321
|
-
|
|
326
|
+
/**
|
|
327
|
+
* TODOS los permisos que el acta reconoce para un aparato, en el orden en que se leen: de
|
|
328
|
+
* lo cotidiano a lo que cambia quién manda.
|
|
329
|
+
*
|
|
330
|
+
* `approve` y `sealer` faltaban, y solo se podían conceder por la CLI. Un permiso que
|
|
331
|
+
* existe y no se ve en la pantalla que se llama «permisos» hace creer que no existe — y
|
|
332
|
+
* `sealer` es justo el que hay que entender para el multivault. Se ven siempre, aunque en
|
|
333
|
+
* un teléfono normal `sealer` no lo vaya a usar nadie: verlo apagado explica el modelo
|
|
334
|
+
* (dueño, 2026-08-31).
|
|
335
|
+
*/
|
|
336
|
+
const CAPS_ORDER = ['sign', 'store', 'read', 'admin', 'approve', 'passwords', 'sealer']
|
|
322
337
|
|
|
323
338
|
function capsRows (st, t) {
|
|
324
339
|
const i = L(st)
|
|
@@ -537,6 +552,28 @@ function autoLockWakeIn (st, now = Date.now()) {
|
|
|
537
552
|
* reabriría sola en cuanto alguien pulsara Enter (ver `reunlockSilently`), así que el
|
|
538
553
|
* candado del daemon no habría cerrado nada.
|
|
539
554
|
*/
|
|
555
|
+
/** Cada cuánto se le dice al daemon «sigo aquí». Muy por debajo del plazo del candado. */
|
|
556
|
+
const TOUCH_MS = 60_000
|
|
557
|
+
let ultimoToque = 0
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Le dice al daemon que sigues delante, sin pedirle nada. Se llama al teclear.
|
|
561
|
+
*
|
|
562
|
+
* No se espera (`await`): estirar el candado no puede meterse en medio de lo que estabas
|
|
563
|
+
* haciendo, y si se pierde un aviso, el siguiente llega un minuto después — con cinco de
|
|
564
|
+
* plazo, no pasa nada.
|
|
565
|
+
*/
|
|
566
|
+
function seguirAqui (st, api = vc) {
|
|
567
|
+
const ahora = Date.now()
|
|
568
|
+
if (ahora - ultimoToque < TOUCH_MS) return
|
|
569
|
+
const p = (st.state?.profiles || []).find((x) => x.current)
|
|
570
|
+
// Solo tiene sentido en un perfil con contraseña y abierto: sin candado no hay plazo
|
|
571
|
+
// que estirar, y cerrado no hay nada que mantener abierto.
|
|
572
|
+
if (!p?.protected || p.locked) return
|
|
573
|
+
ultimoToque = ahora
|
|
574
|
+
try { Promise.resolve(api.touchProfile(p.id)).catch(() => {}) } catch (_) {}
|
|
575
|
+
}
|
|
576
|
+
|
|
540
577
|
async function forgetAutoLocked (term, st, api = vc) {
|
|
541
578
|
const ids = autoLockedIds(st)
|
|
542
579
|
if (!ids.length) return false
|
|
@@ -1728,6 +1765,13 @@ export async function runTui () {
|
|
|
1728
1765
|
if (st.confirm) { await onConfirmKey(st, key); continue }
|
|
1729
1766
|
if (key.name === 'ctrl-c') { running = false; continue }
|
|
1730
1767
|
|
|
1768
|
+
// TECLEAR ES USO. El candado se cierra a los 5 min de no usarse, y hasta ahora
|
|
1769
|
+
// «usar» solo contaba cuando algo pedía al daemon: podías estar media hora
|
|
1770
|
+
// navegando esta pantalla y se cerraba igual, que es lo contrario de lo que se
|
|
1771
|
+
// quería. Se avisa como mucho una vez por minuto —el plazo son cinco, así que
|
|
1772
|
+
// sobra— para no escribir un archivo por tecla.
|
|
1773
|
+
seguirAqui(st)
|
|
1774
|
+
|
|
1731
1775
|
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
1732
1776
|
// 'q' global sale.
|
|
1733
1777
|
if (ch === 'q') { running = false; continue }
|
|
@@ -1770,4 +1814,4 @@ export async function runTui () {
|
|
|
1770
1814
|
}
|
|
1771
1815
|
|
|
1772
1816
|
// Solo para pruebas headless (render sin terminal real). No usar en runtime.
|
|
1773
|
-
export const __test = { render, activeLocked, autoLockedIds, autoLockWakeIn, forgetAutoLocked, autoLockMin, refreshAll, ensureUnlocked, profileRows, deviceRows, secretRows, devVarRows, meRows, capsRows, pairModeRows, pairingBody, scrollBody, fitHelp, toggleLang, mergeMembersAndCerts }
|
|
1817
|
+
export const __test = { render, activeLocked, autoLockedIds, autoLockWakeIn, forgetAutoLocked, autoLockMin, refreshAll, ensureUnlocked, profileRows, deviceRows, secretRows, devVarRows, meRows, capsRows, pairModeRows, pairingBody, scrollBody, fitHelp, toggleLang, mergeMembersAndCerts, seguirAqui, resetToque: () => { ultimoToque = 0 } }
|
package/src/tui/i18n.js
CHANGED
|
@@ -46,14 +46,22 @@ const es = {
|
|
|
46
46
|
store: 'Guardar tus datos',
|
|
47
47
|
read: 'Leer tus datos',
|
|
48
48
|
admin: 'Administrar el perfil',
|
|
49
|
-
passwords: 'Pedir tus contraseñas'
|
|
49
|
+
passwords: 'Pedir tus contraseñas',
|
|
50
|
+
approve: 'Aprobar pedidos',
|
|
51
|
+
sealer: 'Sellar el acta'
|
|
50
52
|
},
|
|
51
53
|
capHint: {
|
|
52
54
|
sign: 'usar tu identidad en las apps del ecosistema',
|
|
53
55
|
store: 'escribir en tu bóveda (perfil, contenido, datos sensibles)',
|
|
54
56
|
read: 'ver lo que guardaste',
|
|
55
57
|
admin: 'conectar y quitar dispositivos desde ese aparato, sin venir aquí',
|
|
56
|
-
passwords: 'el gestor: pedir una credencial por sitio, nunca la lista entera'
|
|
58
|
+
passwords: 'el gestor: pedir una credencial por sitio, nunca la lista entera',
|
|
59
|
+
approve: 'dar el visto bueno cuando otro aparato pide claves privadas',
|
|
60
|
+
// Sellar solo significa algo en un aparato que HAGA de bóveda (otra máquina con el
|
|
61
|
+
// daemon, o un navegador que se puso de bóveda). En un teléfono normal se concede y no
|
|
62
|
+
// lo usa nadie. Se enseña igual, y se dice: verlo desactivado explica el modelo;
|
|
63
|
+
// esconderlo hacía creer que no existe (dueño, 2026-08-31).
|
|
64
|
+
sealer: 'admitir aparatos y cambiar permisos él solo, si esta bóveda no está. Solo sirve en otra BÓVEDA'
|
|
57
65
|
},
|
|
58
66
|
confirmAdmin: (id) => `¿Dejar que ${id} conecte y quite dispositivos sin venir aquí?`,
|
|
59
67
|
capGiven: (n) => `Concedido: ${n}`,
|
|
@@ -140,6 +148,7 @@ const es = {
|
|
|
140
148
|
cantRemoveMaster: 'Esta bóveda es la que manda: no se quita a sí misma.',
|
|
141
149
|
revokedCount: (n) => ` Revocados: ${n}`,
|
|
142
150
|
startingPairing: 'Iniciando emparejamiento…',
|
|
151
|
+
deviceSince: 'desde',
|
|
143
152
|
nameDeviceLabel: '¿Cómo se va a llamar este aparato?',
|
|
144
153
|
nameDeviceHint: 'Enter para dejar que lo diga él (usará tu apodo, y así no se distinguen)',
|
|
145
154
|
noPending: 'No hay ningún dispositivo pendiente',
|
|
@@ -347,14 +356,18 @@ const en = {
|
|
|
347
356
|
store: 'Save your data',
|
|
348
357
|
read: 'Read your data',
|
|
349
358
|
admin: 'Manage the profile',
|
|
350
|
-
passwords: 'Ask for your passwords'
|
|
359
|
+
passwords: 'Ask for your passwords',
|
|
360
|
+
approve: 'Approve requests',
|
|
361
|
+
sealer: 'Seal the record'
|
|
351
362
|
},
|
|
352
363
|
capHint: {
|
|
353
364
|
sign: 'use your identity across the ecosystem apps',
|
|
354
365
|
store: 'write to your vault (profile, content, sensitive data)',
|
|
355
366
|
read: 'see what you saved',
|
|
356
367
|
admin: 'connect and remove devices from that device, without coming here',
|
|
357
|
-
passwords: 'the manager: ask for one credential per site, never the whole list'
|
|
368
|
+
passwords: 'the manager: ask for one credential per site, never the whole list',
|
|
369
|
+
approve: 'give the go-ahead when another device asks for private keys',
|
|
370
|
+
sealer: 'admit devices and change permissions on its own if this vault is gone. Only useful on another VAULT'
|
|
358
371
|
},
|
|
359
372
|
confirmAdmin: (id) => `Let ${id} connect and remove devices without coming here?`,
|
|
360
373
|
capGiven: (n) => `Granted: ${n}`,
|
|
@@ -437,6 +450,7 @@ const en = {
|
|
|
437
450
|
cantRemoveMaster: 'This vault is the Master: it does not remove itself.',
|
|
438
451
|
revokedCount: (n) => ` Revoked: ${n}`,
|
|
439
452
|
startingPairing: 'Starting pairing…',
|
|
453
|
+
deviceSince: 'since',
|
|
440
454
|
nameDeviceLabel: 'What will this device be called?',
|
|
441
455
|
nameDeviceHint: 'Enter to let it say (it will use your nickname, and then they all look alike)',
|
|
442
456
|
noPending: 'No device is waiting',
|
package/src/vaultControl.js
CHANGED
|
@@ -224,6 +224,15 @@ export const renameProfile = (profile, name) => profileOp('rename', { profile, n
|
|
|
224
224
|
export const removeProfile = (profile) => profileOp('rm', { profile })
|
|
225
225
|
export const unlockProfile = (profile, password) => profileOp('unlock', { profile, password })
|
|
226
226
|
export const lockProfile = (profile) => profileOp('lock', { profile })
|
|
227
|
+
/**
|
|
228
|
+
* «Sigo aquí»: estira el plazo del bloqueo automático sin hacer nada más.
|
|
229
|
+
*
|
|
230
|
+
* La pantalla de control la usa al teclear. Sin esto, moverse por ella no tocaba el
|
|
231
|
+
* daemon —solo lo hacían las acciones—, así que el candado se cerraba a los 5 minutos de
|
|
232
|
+
* ABRIRLO aunque estuvieras delante. El plazo se cuenta desde el último uso, y teclear es
|
|
233
|
+
* uso.
|
|
234
|
+
*/
|
|
235
|
+
export const touchProfile = (profile) => profileOp('touch', { profile })
|
|
227
236
|
// `current`: la contraseña que YA tenía el perfil. Hace falta para abrir la copia
|
|
228
237
|
// maestra de los secretos y volver a cerrarla con la nueva — sin ese paso, cambiar la
|
|
229
238
|
// contraseña dejaría las variables privadas ilegibles.
|