@dotrino/vaultd 0.79.0 → 0.80.1
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 +2 -2
- package/src/ctl.js +5 -1
- package/src/tui/app.js +89 -30
- package/src/tui/i18n.js +17 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.1",
|
|
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": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"node": ">=20"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@dotrino/identity": "0.
|
|
23
|
+
"@dotrino/identity": "0.76.0",
|
|
24
24
|
"@dotrino/passmanager": "^0.9.0",
|
|
25
25
|
"@dotrino/proxy-client": "^0.13.1",
|
|
26
26
|
"ws": "^8.18.0"
|
package/src/ctl.js
CHANGED
|
@@ -546,7 +546,11 @@ async function cmdMembers () {
|
|
|
546
546
|
const CAP = {
|
|
547
547
|
sign: 'firma', store: 'guarda', read: 'lee', secrets: 'lee sus claves',
|
|
548
548
|
admin: `${B}administra el perfil${Z}`, approve: `${B}aprueba pedidos${Z}`,
|
|
549
|
-
sealer: `${B}sella el acta${Z}`, passwords: `${B}pide contraseñas${Z}
|
|
549
|
+
sealer: `${B}sella el acta${Z}`, passwords: `${B}pide contraseñas${Z}`,
|
|
550
|
+
// Resaltado como los otros cuatro: dice si esa máquina se lleva tus claves privadas
|
|
551
|
+
// sola o si te lo pregunta. Sin traducción salía el identificador en crudo
|
|
552
|
+
// (`unattended`) en una lista que lee un humano.
|
|
553
|
+
unattended: `${B}recibe claves sin aprobación${Z}`
|
|
550
554
|
}
|
|
551
555
|
// El nombre del perfil es una pubkey JWK. Recortarla no la hace legible: la deja
|
|
552
556
|
// pareciendo un error (`{"key_ops":["verify"],"e…`). Se muestra su huella corta, la
|
package/src/tui/app.js
CHANGED
|
@@ -340,6 +340,19 @@ function pairModeRows (st, t) {
|
|
|
340
340
|
*/
|
|
341
341
|
const CAPS_ORDER = ['sign', 'store', 'read', 'admin', 'approve', 'passwords', 'sealer', 'unattended']
|
|
342
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Los permisos que se le pueden dar A ESTE miembro, en orden.
|
|
345
|
+
*
|
|
346
|
+
* `secrets` solo existe para un SERVICIO (los que tienen `cn`), y por eso faltaba: la
|
|
347
|
+
* lista era fija y no cabía algo que depende de quién sea. Pero para un servicio es EL
|
|
348
|
+
* permiso —el que decide si abre su cajón— y no verlo aquí hacía creer que no existe,
|
|
349
|
+
* igual que pasaba con `unattended`. Se enseña primero, que es su importancia real.
|
|
350
|
+
*
|
|
351
|
+
* A un aparato SIN `cn` no se le ofrece: no hay cajón que abrir, el acta lo filtra
|
|
352
|
+
* (`allowedCaps`) y enseñar una casilla que no hace nada es peor que no enseñarla.
|
|
353
|
+
*/
|
|
354
|
+
const capsForMember = (member) => (member?.cn ? ['secrets', ...CAPS_ORDER] : CAPS_ORDER)
|
|
355
|
+
|
|
343
356
|
function capsRows (st, t) {
|
|
344
357
|
const i = L(st)
|
|
345
358
|
const target = st.capsFor
|
|
@@ -347,20 +360,34 @@ function capsRows (st, t) {
|
|
|
347
360
|
const member = (st.members || []).find((m) => m.pub === target.pub)
|
|
348
361
|
if (!member) return [{ text: t.muted(i.capsNoMember), sel: false }]
|
|
349
362
|
|
|
350
|
-
|
|
363
|
+
// EL BORRADOR manda sobre lo que hay en el acta: es lo que estás a punto de dejar.
|
|
364
|
+
const real = new Set(member.caps || [])
|
|
365
|
+
const has = st.capsDraft?.pub === member.pub ? new Set(st.capsDraft.caps) : real
|
|
366
|
+
|
|
351
367
|
const rows = [
|
|
352
368
|
{ text: ' ' + t.bold(i.capsFor(target.deviceId, member.label || '')), sel: false },
|
|
353
369
|
{ text: '', sel: false }
|
|
354
370
|
]
|
|
355
|
-
|
|
371
|
+
let tocados = 0
|
|
372
|
+
for (const cap of capsForMember(member)) {
|
|
356
373
|
const mark = has.has(cap) ? '[x]' : '[ ]'
|
|
357
|
-
|
|
358
|
-
|
|
374
|
+
// El nombre de `secrets` lleva el cajón dentro: «las claves de proxy» dice qué abre,
|
|
375
|
+
// «lee sus claves» te deja preguntándote cuáles.
|
|
376
|
+
const name = typeof i.capName[cap] === 'function' ? i.capName[cap](member.cn) : i.capName[cap]
|
|
377
|
+
// LO QUE CAMBIA SE SEÑALA. Sin esto el borrador es indistinguible del acta y no sabes
|
|
378
|
+
// qué vas a firmar — que es justo lo que hace peligroso acumular cambios.
|
|
379
|
+
const cambia = has.has(cap) !== real.has(cap)
|
|
380
|
+
if (cambia) tocados++
|
|
381
|
+
const line = ` ${mark}${cambia ? t.bold('*') : ' '} ${cap === 'admin' ? t.bold(name) : name}`
|
|
359
382
|
rows.push({ text: line, sel: true, meta: { cap } })
|
|
360
|
-
|
|
383
|
+
const hint = typeof i.capHint[cap] === 'function' ? i.capHint[cap](member.cn) : i.capHint[cap]
|
|
384
|
+
rows.push({ text: t.muted(' ' + hint), sel: false })
|
|
361
385
|
}
|
|
362
386
|
rows.push({ text: '', sel: false })
|
|
363
|
-
rows.push({
|
|
387
|
+
rows.push({
|
|
388
|
+
text: tocados ? ' ' + t.bold(i.capsPending(tocados)) : t.muted(' ' + i.capsApplyHint),
|
|
389
|
+
sel: false
|
|
390
|
+
})
|
|
364
391
|
return rows
|
|
365
392
|
}
|
|
366
393
|
|
|
@@ -929,6 +956,8 @@ async function onKeyDevices (term, st, key) {
|
|
|
929
956
|
})
|
|
930
957
|
} else if (ch === 'c' && cur?.sub) {
|
|
931
958
|
st.capsFor = { pub: cur.sub, deviceId: cur.deviceId }
|
|
959
|
+
// Borrador limpio al entrar: arrastrar el de otro aparato sería firmar lo que no viste.
|
|
960
|
+
st.capsDraft = null
|
|
932
961
|
st.sel.caps = 0
|
|
933
962
|
await refreshMembers(term, st)
|
|
934
963
|
st.screen = 'caps'
|
|
@@ -1003,9 +1032,19 @@ async function beginPairing (term, st, profile, service = null, label = null) {
|
|
|
1003
1032
|
}
|
|
1004
1033
|
|
|
1005
1034
|
/**
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1035
|
+
* UN BORRADOR QUE SE GUARDA O SE DESCARTA (dueño, 2026-09-01).
|
|
1036
|
+
*
|
|
1037
|
+
* Antes cada Enter sellaba un acta. Cambiar cuatro permisos eran cuatro actas, cuatro
|
|
1038
|
+
* avisos a todos los aparatos y cuatro renovaciones de certificado — absurdo, y encima
|
|
1039
|
+
* dejaba la cuenta pasando por estados intermedios que nadie quiso (un aparato con
|
|
1040
|
+
* `admin` pero todavía sin `read`, por ejemplo).
|
|
1041
|
+
*
|
|
1042
|
+
* Ahora Enter mueve un borrador LOCAL —nada firmado, nada avisado— y `G` lo aplica de
|
|
1043
|
+
* golpe. `applyChanges` ya tomaba una LISTA de cambios y producía UNA acta: el borrador es
|
|
1044
|
+
* esa lista, sin firmar, hasta que confirmas.
|
|
1045
|
+
*
|
|
1046
|
+
* Lo que se cambia va marcado con `*` y contado abajo: un borrador que no se distingue del
|
|
1047
|
+
* acta es peor que no tenerlo, porque firmas sin saber qué.
|
|
1009
1048
|
*/
|
|
1010
1049
|
async function onKeyCaps (term, st, key) {
|
|
1011
1050
|
const i = L(st)
|
|
@@ -1015,31 +1054,51 @@ async function onKeyCaps (term, st, key) {
|
|
|
1015
1054
|
const cur = sels[Math.min(st.sel.caps || 0, sels.length - 1)]
|
|
1016
1055
|
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
1017
1056
|
|
|
1018
|
-
if (key.name === 'escape' || ch === 'b') { st.screen = 'devices'; st.capsFor = null; return true }
|
|
1019
|
-
if (key.name === 'f5') { await refreshMembers(term, st); return true }
|
|
1020
|
-
if ((key.name !== 'enter' && ch !== ' ') || !cur) return true
|
|
1021
|
-
|
|
1022
1057
|
const member = (st.members || []).find((m) => m.pub === st.capsFor?.pub)
|
|
1023
|
-
if (!member)
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
if (giving) caps.add(cur.cap); else caps.delete(cur.cap)
|
|
1027
|
-
|
|
1028
|
-
const apply = async () => {
|
|
1029
|
-
const r = await guard(term, st, i.applyingCaps, () => vc.setDeviceCaps(member.pub, [...caps], activeId(st)))
|
|
1030
|
-
if (!r.ok) return
|
|
1031
|
-
applyDump(st, r.v)
|
|
1032
|
-
await refreshMembers(term, st)
|
|
1033
|
-
flash(st, giving ? i.capGiven(i.capName[cur.cap]) : i.capTaken(i.capName[cur.cap]))
|
|
1058
|
+
if (!member) {
|
|
1059
|
+
if (key.name === 'escape' || ch === 'b') { st.screen = 'devices'; st.capsFor = null }
|
|
1060
|
+
return true
|
|
1034
1061
|
}
|
|
1062
|
+
const real = new Set(member.caps || [])
|
|
1063
|
+
if (st.capsDraft?.pub !== member.pub) st.capsDraft = { pub: member.pub, caps: [...real] }
|
|
1064
|
+
const draft = new Set(st.capsDraft.caps)
|
|
1065
|
+
const sucio = [...new Set([...draft, ...real])].some((c) => draft.has(c) !== real.has(c))
|
|
1035
1066
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1067
|
+
const salir = () => { st.screen = 'devices'; st.capsFor = null; st.capsDraft = null }
|
|
1068
|
+
if (key.name === 'escape' || ch === 'b') {
|
|
1069
|
+
// SALIR CON CAMBIOS SIN GUARDAR SE PREGUNTA. Tirarlos callando es perder trabajo sin
|
|
1070
|
+
// decirlo, y es justo lo que un borrador no debe hacer.
|
|
1071
|
+
if (sucio) setConfirm(st, { text: i.capsDiscard, onYes: async () => { salir() } })
|
|
1072
|
+
else salir()
|
|
1040
1073
|
return true
|
|
1041
1074
|
}
|
|
1042
|
-
await
|
|
1075
|
+
if (key.name === 'f5') { st.capsDraft = null; await refreshMembers(term, st); return true }
|
|
1076
|
+
|
|
1077
|
+
// GUARDAR: UNA sola acta con todo lo que hayas tocado.
|
|
1078
|
+
if (ch === 'g') {
|
|
1079
|
+
if (!sucio) { flash(st, i.capsNothing); return true }
|
|
1080
|
+
const cuantos = [...new Set([...draft, ...real])].filter((c) => draft.has(c) !== real.has(c)).length
|
|
1081
|
+
const guardar = async () => {
|
|
1082
|
+
const r = await guard(term, st, i.applyingCaps, () => vc.setDeviceCaps(member.pub, [...draft], activeId(st)))
|
|
1083
|
+
if (!r.ok) return
|
|
1084
|
+
applyDump(st, r.v)
|
|
1085
|
+
st.capsDraft = null
|
|
1086
|
+
await refreshMembers(term, st)
|
|
1087
|
+
flash(st, i.capsSaved(cuantos))
|
|
1088
|
+
}
|
|
1089
|
+
// Administrar se PREGUNTA, y se pregunta AL GUARDAR: es cuando pasa a ser verdad.
|
|
1090
|
+
if (draft.has('admin') && !real.has('admin')) {
|
|
1091
|
+
setConfirm(st, { text: i.confirmAdmin(st.capsFor.deviceId), onYes: guardar })
|
|
1092
|
+
return true
|
|
1093
|
+
}
|
|
1094
|
+
await guardar()
|
|
1095
|
+
return true
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if ((key.name !== 'enter' && ch !== ' ') || !cur) return true
|
|
1099
|
+
// Enter solo mueve el BORRADOR: no firma, no avisa, no se puede equivocar caro.
|
|
1100
|
+
if (draft.has(cur.cap)) draft.delete(cur.cap); else draft.add(cur.cap)
|
|
1101
|
+
st.capsDraft = { pub: member.pub, caps: [...draft] }
|
|
1043
1102
|
return true
|
|
1044
1103
|
}
|
|
1045
1104
|
|
|
@@ -1819,4 +1878,4 @@ export async function runTui () {
|
|
|
1819
1878
|
}
|
|
1820
1879
|
|
|
1821
1880
|
// Solo para pruebas headless (render sin terminal real). No usar en runtime.
|
|
1822
|
-
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 } }
|
|
1881
|
+
export const __test = { render, activeLocked, autoLockedIds, autoLockWakeIn, forgetAutoLocked, autoLockMin, refreshAll, ensureUnlocked, profileRows, deviceRows, secretRows, devVarRows, meRows, capsRows, onKeyCaps, pairModeRows, pairingBody, scrollBody, fitHelp, toggleLang, mergeMembersAndCerts, seguirAqui, resetToque: () => { ultimoToque = 0 } }
|
package/src/tui/i18n.js
CHANGED
|
@@ -40,8 +40,15 @@ const es = {
|
|
|
40
40
|
titleCaps: 'Permisos del dispositivo',
|
|
41
41
|
capsFor: (id, name) => `Permisos de ${id}${name ? ' · ' + name : ''}`,
|
|
42
42
|
capsNoMember: ' (este dispositivo ya no está en el acta)',
|
|
43
|
-
capsApplyHint: 'Enter marca o desmarca
|
|
43
|
+
capsApplyHint: 'Enter marca o desmarca · G guarda los cambios · Esc sale',
|
|
44
|
+
capsPending: (n) => `${n} cambio(s) sin guardar (*) · G para guardarlos en UNA sola acta · Esc para descartarlos`,
|
|
45
|
+
capsSaved: (n) => `${n} cambio(s) guardados en una sola acta`,
|
|
46
|
+
capsNothing: 'No has cambiado nada',
|
|
47
|
+
capsDiscard: '¿Descartar los cambios sin guardar?',
|
|
44
48
|
capName: {
|
|
49
|
+
// Lleva el cajón dentro: «las claves de proxy» dice QUÉ abre; «lee sus claves» te deja
|
|
50
|
+
// preguntándote cuáles. Solo se le ofrece a un servicio (los que tienen `cn`).
|
|
51
|
+
secrets: (cn) => `Leer las claves de «${cn}»`,
|
|
45
52
|
sign: 'Firmar como tú',
|
|
46
53
|
store: 'Guardar tus datos',
|
|
47
54
|
read: 'Leer tus datos',
|
|
@@ -52,6 +59,7 @@ const es = {
|
|
|
52
59
|
unattended: 'Recibir claves sin aprobación'
|
|
53
60
|
},
|
|
54
61
|
capHint: {
|
|
62
|
+
secrets: (cn) => `abrir SU cajón y ninguno más: las variables de «${cn}»`,
|
|
55
63
|
sign: 'usar tu identidad en las apps del ecosistema',
|
|
56
64
|
store: 'escribir en tu bóveda (perfil, contenido, datos sensibles)',
|
|
57
65
|
read: 'ver lo que guardaste',
|
|
@@ -70,7 +78,7 @@ const es = {
|
|
|
70
78
|
capTaken: (n) => `Quitado: ${n}`,
|
|
71
79
|
applyingCaps: 'Aplicando…',
|
|
72
80
|
loadingMembers: 'Cargando el acta…',
|
|
73
|
-
helpCaps: ['↑↓', 'Enter marcar', 'F5 refrescar', 'Esc dispositivos', 'l English', 'q salir'],
|
|
81
|
+
helpCaps: ['↑↓', 'Enter marcar', 'G guardar', 'F5 refrescar', 'Esc dispositivos', 'l English', 'q salir'],
|
|
74
82
|
// Perfil del usuario (lo que sincronizan los dispositivos). Solo lectura: se edita en
|
|
75
83
|
// el aparato, no en la máquina donde vive la bóveda.
|
|
76
84
|
loadingProfile: 'Cargando el perfil…',
|
|
@@ -352,8 +360,13 @@ const en = {
|
|
|
352
360
|
titleCaps: 'Device permissions',
|
|
353
361
|
capsFor: (id, name) => `Permissions for ${id}${name ? ' · ' + name : ''}`,
|
|
354
362
|
capsNoMember: ' (this device is no longer in the record)',
|
|
355
|
-
capsApplyHint: 'Enter ticks or unticks
|
|
363
|
+
capsApplyHint: 'Enter ticks or unticks · G saves · Esc leaves',
|
|
364
|
+
capsPending: (n) => `${n} unsaved change(s) (*) · G to save them in ONE record · Esc to discard`,
|
|
365
|
+
capsSaved: (n) => `${n} change(s) saved in a single record`,
|
|
366
|
+
capsNothing: 'You have not changed anything',
|
|
367
|
+
capsDiscard: 'Discard the unsaved changes?',
|
|
356
368
|
capName: {
|
|
369
|
+
secrets: (cn) => `Read the «${cn}» keys`,
|
|
357
370
|
sign: 'Sign as you',
|
|
358
371
|
store: 'Save your data',
|
|
359
372
|
read: 'Read your data',
|
|
@@ -378,7 +391,7 @@ const en = {
|
|
|
378
391
|
capTaken: (n) => `Removed: ${n}`,
|
|
379
392
|
applyingCaps: 'Applying…',
|
|
380
393
|
loadingMembers: 'Loading the record…',
|
|
381
|
-
helpCaps: ['↑↓', 'Enter tick', 'F5 refresh', 'Esc devices', 'l Español', 'q quit'],
|
|
394
|
+
helpCaps: ['↑↓', 'Enter tick', 'G save', 'F5 refresh', 'Esc devices', 'l Español', 'q quit'],
|
|
382
395
|
loadingProfile: 'Loading profile…',
|
|
383
396
|
noProfile: ' (this vault has no profile yet)',
|
|
384
397
|
noProfileHint: ' Edit your name or photo on a paired device and press F5.',
|