@dotrino/vaultd 0.79.0 → 0.80.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 +2 -2
- package/src/ctl.js +5 -1
- package/src/tui/app.js +70 -27
- package/src/tui/i18n.js +12 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.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": {
|
|
@@ -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
|
@@ -347,20 +347,31 @@ function capsRows (st, t) {
|
|
|
347
347
|
const member = (st.members || []).find((m) => m.pub === target.pub)
|
|
348
348
|
if (!member) return [{ text: t.muted(i.capsNoMember), sel: false }]
|
|
349
349
|
|
|
350
|
-
|
|
350
|
+
// EL BORRADOR manda sobre lo que hay en el acta: es lo que estás a punto de dejar.
|
|
351
|
+
const real = new Set(member.caps || [])
|
|
352
|
+
const has = st.capsDraft?.pub === member.pub ? new Set(st.capsDraft.caps) : real
|
|
353
|
+
|
|
351
354
|
const rows = [
|
|
352
355
|
{ text: ' ' + t.bold(i.capsFor(target.deviceId, member.label || '')), sel: false },
|
|
353
356
|
{ text: '', sel: false }
|
|
354
357
|
]
|
|
358
|
+
let tocados = 0
|
|
355
359
|
for (const cap of CAPS_ORDER) {
|
|
356
360
|
const mark = has.has(cap) ? '[x]' : '[ ]'
|
|
357
361
|
const name = i.capName[cap]
|
|
358
|
-
|
|
362
|
+
// LO QUE CAMBIA SE SEÑALA. Sin esto el borrador es indistinguible del acta y no sabes
|
|
363
|
+
// qué vas a firmar — que es justo lo que hace peligroso acumular cambios.
|
|
364
|
+
const cambia = has.has(cap) !== real.has(cap)
|
|
365
|
+
if (cambia) tocados++
|
|
366
|
+
const line = ` ${mark}${cambia ? t.bold('*') : ' '} ${cap === 'admin' ? t.bold(name) : name}`
|
|
359
367
|
rows.push({ text: line, sel: true, meta: { cap } })
|
|
360
368
|
rows.push({ text: t.muted(' ' + i.capHint[cap]), sel: false })
|
|
361
369
|
}
|
|
362
370
|
rows.push({ text: '', sel: false })
|
|
363
|
-
rows.push({
|
|
371
|
+
rows.push({
|
|
372
|
+
text: tocados ? ' ' + t.bold(i.capsPending(tocados)) : t.muted(' ' + i.capsApplyHint),
|
|
373
|
+
sel: false
|
|
374
|
+
})
|
|
364
375
|
return rows
|
|
365
376
|
}
|
|
366
377
|
|
|
@@ -929,6 +940,8 @@ async function onKeyDevices (term, st, key) {
|
|
|
929
940
|
})
|
|
930
941
|
} else if (ch === 'c' && cur?.sub) {
|
|
931
942
|
st.capsFor = { pub: cur.sub, deviceId: cur.deviceId }
|
|
943
|
+
// Borrador limpio al entrar: arrastrar el de otro aparato sería firmar lo que no viste.
|
|
944
|
+
st.capsDraft = null
|
|
932
945
|
st.sel.caps = 0
|
|
933
946
|
await refreshMembers(term, st)
|
|
934
947
|
st.screen = 'caps'
|
|
@@ -1003,9 +1016,19 @@ async function beginPairing (term, st, profile, service = null, label = null) {
|
|
|
1003
1016
|
}
|
|
1004
1017
|
|
|
1005
1018
|
/**
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1019
|
+
* UN BORRADOR QUE SE GUARDA O SE DESCARTA (dueño, 2026-09-01).
|
|
1020
|
+
*
|
|
1021
|
+
* Antes cada Enter sellaba un acta. Cambiar cuatro permisos eran cuatro actas, cuatro
|
|
1022
|
+
* avisos a todos los aparatos y cuatro renovaciones de certificado — absurdo, y encima
|
|
1023
|
+
* dejaba la cuenta pasando por estados intermedios que nadie quiso (un aparato con
|
|
1024
|
+
* `admin` pero todavía sin `read`, por ejemplo).
|
|
1025
|
+
*
|
|
1026
|
+
* Ahora Enter mueve un borrador LOCAL —nada firmado, nada avisado— y `G` lo aplica de
|
|
1027
|
+
* golpe. `applyChanges` ya tomaba una LISTA de cambios y producía UNA acta: el borrador es
|
|
1028
|
+
* esa lista, sin firmar, hasta que confirmas.
|
|
1029
|
+
*
|
|
1030
|
+
* Lo que se cambia va marcado con `*` y contado abajo: un borrador que no se distingue del
|
|
1031
|
+
* acta es peor que no tenerlo, porque firmas sin saber qué.
|
|
1009
1032
|
*/
|
|
1010
1033
|
async function onKeyCaps (term, st, key) {
|
|
1011
1034
|
const i = L(st)
|
|
@@ -1015,31 +1038,51 @@ async function onKeyCaps (term, st, key) {
|
|
|
1015
1038
|
const cur = sels[Math.min(st.sel.caps || 0, sels.length - 1)]
|
|
1016
1039
|
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
1017
1040
|
|
|
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
1041
|
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]))
|
|
1042
|
+
if (!member) {
|
|
1043
|
+
if (key.name === 'escape' || ch === 'b') { st.screen = 'devices'; st.capsFor = null }
|
|
1044
|
+
return true
|
|
1034
1045
|
}
|
|
1046
|
+
const real = new Set(member.caps || [])
|
|
1047
|
+
if (st.capsDraft?.pub !== member.pub) st.capsDraft = { pub: member.pub, caps: [...real] }
|
|
1048
|
+
const draft = new Set(st.capsDraft.caps)
|
|
1049
|
+
const sucio = [...new Set([...draft, ...real])].some((c) => draft.has(c) !== real.has(c))
|
|
1035
1050
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1051
|
+
const salir = () => { st.screen = 'devices'; st.capsFor = null; st.capsDraft = null }
|
|
1052
|
+
if (key.name === 'escape' || ch === 'b') {
|
|
1053
|
+
// SALIR CON CAMBIOS SIN GUARDAR SE PREGUNTA. Tirarlos callando es perder trabajo sin
|
|
1054
|
+
// decirlo, y es justo lo que un borrador no debe hacer.
|
|
1055
|
+
if (sucio) setConfirm(st, { text: i.capsDiscard, onYes: async () => { salir() } })
|
|
1056
|
+
else salir()
|
|
1040
1057
|
return true
|
|
1041
1058
|
}
|
|
1042
|
-
await
|
|
1059
|
+
if (key.name === 'f5') { st.capsDraft = null; await refreshMembers(term, st); return true }
|
|
1060
|
+
|
|
1061
|
+
// GUARDAR: UNA sola acta con todo lo que hayas tocado.
|
|
1062
|
+
if (ch === 'g') {
|
|
1063
|
+
if (!sucio) { flash(st, i.capsNothing); return true }
|
|
1064
|
+
const cuantos = [...new Set([...draft, ...real])].filter((c) => draft.has(c) !== real.has(c)).length
|
|
1065
|
+
const guardar = async () => {
|
|
1066
|
+
const r = await guard(term, st, i.applyingCaps, () => vc.setDeviceCaps(member.pub, [...draft], activeId(st)))
|
|
1067
|
+
if (!r.ok) return
|
|
1068
|
+
applyDump(st, r.v)
|
|
1069
|
+
st.capsDraft = null
|
|
1070
|
+
await refreshMembers(term, st)
|
|
1071
|
+
flash(st, i.capsSaved(cuantos))
|
|
1072
|
+
}
|
|
1073
|
+
// Administrar se PREGUNTA, y se pregunta AL GUARDAR: es cuando pasa a ser verdad.
|
|
1074
|
+
if (draft.has('admin') && !real.has('admin')) {
|
|
1075
|
+
setConfirm(st, { text: i.confirmAdmin(st.capsFor.deviceId), onYes: guardar })
|
|
1076
|
+
return true
|
|
1077
|
+
}
|
|
1078
|
+
await guardar()
|
|
1079
|
+
return true
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if ((key.name !== 'enter' && ch !== ' ') || !cur) return true
|
|
1083
|
+
// Enter solo mueve el BORRADOR: no firma, no avisa, no se puede equivocar caro.
|
|
1084
|
+
if (draft.has(cur.cap)) draft.delete(cur.cap); else draft.add(cur.cap)
|
|
1085
|
+
st.capsDraft = { pub: member.pub, caps: [...draft] }
|
|
1043
1086
|
return true
|
|
1044
1087
|
}
|
|
1045
1088
|
|
|
@@ -1819,4 +1862,4 @@ export async function runTui () {
|
|
|
1819
1862
|
}
|
|
1820
1863
|
|
|
1821
1864
|
// 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 } }
|
|
1865
|
+
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,7 +40,11 @@ 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: {
|
|
45
49
|
sign: 'Firmar como tú',
|
|
46
50
|
store: 'Guardar tus datos',
|
|
@@ -70,7 +74,7 @@ const es = {
|
|
|
70
74
|
capTaken: (n) => `Quitado: ${n}`,
|
|
71
75
|
applyingCaps: 'Aplicando…',
|
|
72
76
|
loadingMembers: 'Cargando el acta…',
|
|
73
|
-
helpCaps: ['↑↓', 'Enter marcar', 'F5 refrescar', 'Esc dispositivos', 'l English', 'q salir'],
|
|
77
|
+
helpCaps: ['↑↓', 'Enter marcar', 'G guardar', 'F5 refrescar', 'Esc dispositivos', 'l English', 'q salir'],
|
|
74
78
|
// Perfil del usuario (lo que sincronizan los dispositivos). Solo lectura: se edita en
|
|
75
79
|
// el aparato, no en la máquina donde vive la bóveda.
|
|
76
80
|
loadingProfile: 'Cargando el perfil…',
|
|
@@ -352,7 +356,11 @@ const en = {
|
|
|
352
356
|
titleCaps: 'Device permissions',
|
|
353
357
|
capsFor: (id, name) => `Permissions for ${id}${name ? ' · ' + name : ''}`,
|
|
354
358
|
capsNoMember: ' (this device is no longer in the record)',
|
|
355
|
-
capsApplyHint: 'Enter ticks or unticks
|
|
359
|
+
capsApplyHint: 'Enter ticks or unticks · G saves · Esc leaves',
|
|
360
|
+
capsPending: (n) => `${n} unsaved change(s) (*) · G to save them in ONE record · Esc to discard`,
|
|
361
|
+
capsSaved: (n) => `${n} change(s) saved in a single record`,
|
|
362
|
+
capsNothing: 'You have not changed anything',
|
|
363
|
+
capsDiscard: 'Discard the unsaved changes?',
|
|
356
364
|
capName: {
|
|
357
365
|
sign: 'Sign as you',
|
|
358
366
|
store: 'Save your data',
|
|
@@ -378,7 +386,7 @@ const en = {
|
|
|
378
386
|
capTaken: (n) => `Removed: ${n}`,
|
|
379
387
|
applyingCaps: 'Applying…',
|
|
380
388
|
loadingMembers: 'Loading the record…',
|
|
381
|
-
helpCaps: ['↑↓', 'Enter tick', 'F5 refresh', 'Esc devices', 'l Español', 'q quit'],
|
|
389
|
+
helpCaps: ['↑↓', 'Enter tick', 'G save', 'F5 refresh', 'Esc devices', 'l Español', 'q quit'],
|
|
382
390
|
loadingProfile: 'Loading profile…',
|
|
383
391
|
noProfile: ' (this vault has no profile yet)',
|
|
384
392
|
noProfileHint: ' Edit your name or photo on a paired device and press F5.',
|