@dotrino/vaultd 0.38.0 → 0.49.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/README.md +48 -3
- package/lib/README.md +7 -0
- package/lib/src/admin.js +4 -0
- package/lib/src/atrest.js +0 -0
- package/lib/src/enroll.js +12 -3
- package/lib/src/env.js +2 -2
- package/lib/src/protocol.js +12 -1
- package/lib/src/service.js +355 -54
- package/lib/src/sshAgent.js +100 -0
- package/lib/src/sshKeys.js +76 -0
- package/package.json +6 -6
- package/src/approvals.js +69 -0
- package/src/ctl.js +323 -22
- package/src/daemon.js +174 -15
- package/src/manager.js +6 -0
- package/src/profiles.js +82 -9
- package/src/sealKey.js +80 -0
- package/src/sealer.js +170 -0
- package/src/secretsStore.js +805 -90
- package/src/sshAgent.js +2 -0
- package/src/sshKeys.js +2 -0
- package/src/store.js +3 -1
- package/src/tui/app.js +124 -10
- package/src/tui/i18n.js +33 -6
- package/src/vault.js +839 -35
- package/src/vaultControl.js +50 -14
package/src/sshAgent.js
ADDED
package/src/sshKeys.js
ADDED
package/src/store.js
CHANGED
|
@@ -64,6 +64,8 @@ export function openStore (dir) {
|
|
|
64
64
|
return ok
|
|
65
65
|
},
|
|
66
66
|
getSetting (k) { return data.settings[k] },
|
|
67
|
-
setSetting (k, v) { data.settings[k] = v; save() }
|
|
67
|
+
setSetting (k, v) { if (v === undefined) delete data.settings[k]; else data.settings[k] = v; save() },
|
|
68
|
+
/** Todos los ajustes (copia). Para quien necesita buscar por prefijo. */
|
|
69
|
+
listSettings () { return { ...data.settings } }
|
|
68
70
|
}
|
|
69
71
|
}
|
package/src/tui/app.js
CHANGED
|
@@ -36,6 +36,7 @@ import { createTerm, widthOf } from './term.js'
|
|
|
36
36
|
import { qrToString } from '../qr.js'
|
|
37
37
|
import { dict, otherLang, loadLang, saveLang } from './i18n.js'
|
|
38
38
|
import * as vc from '../vaultControl.js'
|
|
39
|
+
import { VERSION } from '../version.js'
|
|
39
40
|
|
|
40
41
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
41
42
|
|
|
@@ -138,6 +139,13 @@ function activeProfile (st) {
|
|
|
138
139
|
return list.find((p) => p.current) || list[0] || null
|
|
139
140
|
}
|
|
140
141
|
const activeId = (st) => activeProfile(st)?.id || undefined
|
|
142
|
+
/**
|
|
143
|
+
* La contraseña del perfil ACTIVO, que la TUI ya guarda para toda la sesión al
|
|
144
|
+
* desbloquear (ver `reunlockSilently`). Las operaciones que SELLAN una variable la
|
|
145
|
+
* necesitan: sin ella el daemon cae a la llave de la máquina, que no abre la copia
|
|
146
|
+
* maestra de un perfil con contraseña, y la escritura falla con «wrong password».
|
|
147
|
+
*/
|
|
148
|
+
const activePwd = (st) => st.sessionPwd?.get(activeId(st)) || undefined
|
|
141
149
|
|
|
142
150
|
function lockGlyph (p) {
|
|
143
151
|
if (!p?.protected) return ''
|
|
@@ -251,8 +259,12 @@ function deviceRows (st, t) {
|
|
|
251
259
|
for (const d of devices) {
|
|
252
260
|
const label = d.label || t.muted(i.noLabel)
|
|
253
261
|
const vars = devVarsOf(st, d.sub).length
|
|
262
|
+
const debt = debtOf(st, d.sub)
|
|
254
263
|
const extra = (d.certCount > 1 ? t.muted(` certs:${d.certCount}`) : '') +
|
|
255
|
-
(vars ? t.muted(` vars:${vars}`) : '')
|
|
264
|
+
(vars ? t.muted(` vars:${vars}`) : '') +
|
|
265
|
+
// EN DEUDA: en el acta y sin poder abrir lo suyo. Va en color de aviso al lado de
|
|
266
|
+
// sus variables, que es donde se mira cuando algo no arranca.
|
|
267
|
+
(debt ? t.warn(` ${i.deviceDebt(debt)}`) : '')
|
|
256
268
|
// SIN ACCESO: está en el acta y no puede entrar. Es un aviso, no un adorno, así que va
|
|
257
269
|
// en el color de aviso y en el sitio donde estaría su vencimiento.
|
|
258
270
|
const status = d.noAccess
|
|
@@ -341,7 +353,19 @@ function secretRows (st, t) {
|
|
|
341
353
|
// El puntero a la otra pantalla va SIEMPRE, con scopes y sin ellos: es la mitad de la
|
|
342
354
|
// función y quien la busca no tiene por qué adivinar que vive en Dispositivos.
|
|
343
355
|
const footer = [{ text: '', sel: false }, { text: t.muted(' ' + i.devVarsElsewhere), sel: false }]
|
|
344
|
-
|
|
356
|
+
// ARRIBA DEL TODO, antes que las variables: lo que está sin sellar significa que esos
|
|
357
|
+
// aparatos NO están leyendo su configuración ahora mismo, y que solo la contraseña lo
|
|
358
|
+
// arregla. Un aviso que hay que buscar no es un aviso.
|
|
359
|
+
const head = []
|
|
360
|
+
for (const [owner, info] of Object.entries(st.secrets?.pending || {})) {
|
|
361
|
+
const who = (info?.members || []).map((m) => deviceIdOf(st, m.pub) + ' (' + m.keys.join(', ') + ')').join(', ')
|
|
362
|
+
head.push({ text: t.warn(' ' + i.pendingSeal(owner, info?.kind, who)), sel: false })
|
|
363
|
+
}
|
|
364
|
+
const p = activeProfile(st)
|
|
365
|
+
if (p && !p.protected) head.push({ text: t.warn(' ' + i.noPasswordWarn), sel: false })
|
|
366
|
+
if (head.length) head.push({ text: '', sel: false })
|
|
367
|
+
if (!names.length) return [...head, { text: t.muted(i.noScopes), sel: false }, ...footer]
|
|
368
|
+
rows.push(...head)
|
|
345
369
|
for (const n of names) {
|
|
346
370
|
rows.push({ text: t.accent(` ▸ ${n}`) + t.muted(i.scopeOf(n)), sel: true, meta: { ns: n, key: null } })
|
|
347
371
|
for (const k of sortByKey(ns[n])) {
|
|
@@ -354,6 +378,21 @@ function secretRows (st, t) {
|
|
|
354
378
|
/** Las claves guardadas para UN aparato (`pub`), o `[]`. Cada una es `{key, public}`. */
|
|
355
379
|
const devVarsOf = (st, pub) => (st.secrets?.dev || []).find((x) => x.pub === pub)?.keys || []
|
|
356
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Lo que ese aparato NO puede abrir (§8.11). Un servicio que entra después de escrita
|
|
383
|
+
* una variable no tiene envoltura de ella, y hasta que alguien se la reparta está en el
|
|
384
|
+
* acta sin poder arrancar del todo. Aquí se cuenta cuántas, que es lo que cabe en una
|
|
385
|
+
* fila; el detalle está en la consola.
|
|
386
|
+
*/
|
|
387
|
+
/** El ID corto de un aparato a partir de su llave, tal como lo enseña la lista de Aparatos. */
|
|
388
|
+
const deviceIdOf = (st, pub) =>
|
|
389
|
+
(st.devices?.issued || []).find((d) => d.sub === pub)?.deviceId || (st.members || []).find((m) => m.pub === pub)?.id || pub.slice(0, 8)
|
|
390
|
+
|
|
391
|
+
const debtOf = (st, pub) => {
|
|
392
|
+
const d = (st.secrets?.incomplete || []).find((x) => x.pub === pub)
|
|
393
|
+
return d ? [...new Set(Object.values(d.owners || {}).flat())].length : 0
|
|
394
|
+
}
|
|
395
|
+
|
|
357
396
|
const sortByKey = (list) => (list || []).slice().sort((a, b) => a.key.localeCompare(b.key))
|
|
358
397
|
|
|
359
398
|
/**
|
|
@@ -696,7 +735,7 @@ async function onKeyProfiles (term, st, key) {
|
|
|
696
735
|
onSubmit: async (again) => {
|
|
697
736
|
st.input = null
|
|
698
737
|
if (again !== pwd) { flash(st, i.passwordMismatch, 'danger'); return }
|
|
699
|
-
const r = await guard(term, st, i.savingPassword, () => vc.setProfilePassword(p.id, pwd))
|
|
738
|
+
const r = await guard(term, st, i.savingPassword, () => vc.setProfilePassword(p.id, pwd, st.sessionPwd?.get(p.id)))
|
|
700
739
|
// La nueva es la que vale para el resto de la sesión: guardar la vieja dejaría
|
|
701
740
|
// a la TUI reabriendo con una contraseña que ya no existe.
|
|
702
741
|
if (r.ok) { st.sessionPwd?.set(p.id, pwd); flash(st, i.passwordSaved); await refreshProfiles(term, st) }
|
|
@@ -709,7 +748,7 @@ async function onKeyProfiles (term, st, key) {
|
|
|
709
748
|
} else if (ch === 'x' && cur) { // quitar contraseña
|
|
710
749
|
if (!cur.protected) { flash(st, i.noPasswordSet, 'warn'); return true }
|
|
711
750
|
await ensureUnlocked(term, st, cur, async (p = cur) => {
|
|
712
|
-
const r = await guard(term, st, i.removingPassword, () => vc.removeProfilePassword(p.id))
|
|
751
|
+
const r = await guard(term, st, i.removingPassword, () => vc.removeProfilePassword(p.id, st.sessionPwd?.get(p.id)))
|
|
713
752
|
if (r.ok) { st.sessionPwd?.delete(p.id); flash(st, i.passwordRemoved); await refreshProfiles(term, st) }
|
|
714
753
|
})
|
|
715
754
|
} else if (ch === 'u' && cur) {
|
|
@@ -800,6 +839,19 @@ async function onKeyDevices (term, st, key) {
|
|
|
800
839
|
return true
|
|
801
840
|
}
|
|
802
841
|
|
|
842
|
+
/**
|
|
843
|
+
* Tira la cuenta que NACIÓ para un emparejamiento que no llegó a término y vuelve a la que
|
|
844
|
+
* estabas usando. Es una cuenta recién creada y vacía —el aparato nunca entró—, así que no
|
|
845
|
+
* hay nada dentro que perder; lo que sí molesta es que se queden acumulando.
|
|
846
|
+
*/
|
|
847
|
+
async function descartarCuenta (term, st, id, volverA) {
|
|
848
|
+
const i = L(st)
|
|
849
|
+
const r = await guard(term, st, i.discardingAccount, () => vc.removeProfile(id))
|
|
850
|
+
if (volverA) await guard(term, st, i.switchingVault, () => vc.useProfile(volverA))
|
|
851
|
+
await refreshAll(term, st)
|
|
852
|
+
if (r.ok) flash(st, i.accountDiscarded)
|
|
853
|
+
}
|
|
854
|
+
|
|
803
855
|
/**
|
|
804
856
|
* Abre el emparejamiento contra `profile` y salta a la pantalla del QR. Con `service`,
|
|
805
857
|
* el QR es el de un SERVICIO de ese namespace (cert limitado a sus variables).
|
|
@@ -895,6 +947,7 @@ async function onKeyPairMode (term, st, key) {
|
|
|
895
947
|
st.input = null
|
|
896
948
|
const name = raw.trim()
|
|
897
949
|
if (!name) { flash(st, i.nameEmpty, 'danger'); return }
|
|
950
|
+
const previa = activeId(st)
|
|
898
951
|
const r = await guard(term, st, i.creatingVault, () => vc.addProfile(name))
|
|
899
952
|
if (!r.ok) return
|
|
900
953
|
const created = r.v?.id || (r.v?.profiles || []).find((p) => p.name === name)?.id
|
|
@@ -903,7 +956,11 @@ async function onKeyPairMode (term, st, key) {
|
|
|
903
956
|
if (!u.ok) return
|
|
904
957
|
await refreshAll(term, st)
|
|
905
958
|
flash(st, i.accountCreated(name))
|
|
906
|
-
|
|
959
|
+
// Si el emparejamiento no llega a abrirse, la cuenta que se creó PARA él se va con
|
|
960
|
+
// él: si no, cada intento fallido dejaba una cuenta vacía —y encima activa— que
|
|
961
|
+
// luego había que ir a borrar a mano adivinando cuál era.
|
|
962
|
+
if (!await beginPairing(term, st, created)) await descartarCuenta(term, st, created, previa)
|
|
963
|
+
else st.pairing.born = { id: created, from: previa }
|
|
907
964
|
},
|
|
908
965
|
onCancel: () => { st.input = null }
|
|
909
966
|
})
|
|
@@ -922,6 +979,9 @@ function promptApprove (term, st) {
|
|
|
922
979
|
if (r.ok) {
|
|
923
980
|
flash(st, i.deviceApproved)
|
|
924
981
|
st.pending = null
|
|
982
|
+
// El aparato entró: la cuenta que se creó para esto ya es de alguien, así que deja
|
|
983
|
+
// de estar en la lista de las que se descartan al salir.
|
|
984
|
+
st.pairing = null
|
|
925
985
|
st.screen = 'devices'
|
|
926
986
|
// Sin lista (el volcado se perdió): se pide otra vez. El aparato ya está dentro;
|
|
927
987
|
// lo único que falta es la foto, y esa se vuelve a pedir sin drama.
|
|
@@ -963,11 +1023,26 @@ async function onKeyPairing (term, st, key) {
|
|
|
963
1023
|
return true
|
|
964
1024
|
}
|
|
965
1025
|
if (ch === 'r') { // restart: reiniciar el emparejamiento
|
|
1026
|
+
const born = st.pairing?.born
|
|
966
1027
|
const r = await guard(term, st, i.restartingPairing, () => vc.startPairing({ profile: activeId(st) }))
|
|
967
|
-
if (r.ok) { st.pairing = r.v; st.pending = null; st.scroll.pairing = { value: 0 } }
|
|
1028
|
+
if (r.ok) { st.pairing = { ...r.v, ...(born ? { born } : {}) }; st.pending = null; st.scroll.pairing = { value: 0 } }
|
|
968
1029
|
return true
|
|
969
1030
|
}
|
|
970
|
-
if (key.name === 'escape' || ch === 'b') {
|
|
1031
|
+
if (key.name === 'escape' || ch === 'b') {
|
|
1032
|
+
// Te vas sin que nadie haya entrado, y la cuenta se creó PARA esto: se pregunta antes
|
|
1033
|
+
// de dejarla ahí. Es la otra mitad del mismo descuido — con «cuenta nueva» era fácil
|
|
1034
|
+
// acabar con tres cuentas vacías y ninguna forma de saber cuál era cuál.
|
|
1035
|
+
const born = st.pairing?.born
|
|
1036
|
+
if (born) {
|
|
1037
|
+
setConfirm(st, {
|
|
1038
|
+
text: i.confirmDiscardAccount,
|
|
1039
|
+
onYes: async () => { st.confirm = null; st.pairing = null; st.screen = 'devices'; await descartarCuenta(term, st, born.id, born.from) },
|
|
1040
|
+
onNo: async () => { st.confirm = null; st.pairing = null; st.screen = 'devices'; await refreshDevices(term, st) }
|
|
1041
|
+
})
|
|
1042
|
+
return true
|
|
1043
|
+
}
|
|
1044
|
+
st.screen = 'devices'; st.pairing = null; await refreshDevices(term, st)
|
|
1045
|
+
}
|
|
971
1046
|
return true
|
|
972
1047
|
}
|
|
973
1048
|
|
|
@@ -1015,8 +1090,10 @@ async function onKeySecrets (term, st, key) {
|
|
|
1015
1090
|
onNo: () => { st.confirm = null }
|
|
1016
1091
|
})
|
|
1017
1092
|
}
|
|
1093
|
+
} else if (ch === 'v' && cur?.key) {
|
|
1094
|
+
await revealValue(term, st, `ns:${cur.ns}`, cur.key, cur.public)
|
|
1018
1095
|
} else if (ch === 't' && cur?.key) {
|
|
1019
|
-
await toggleVisibility(term, st, cur.public, () => vc.setSecretVisibility(cur.ns, cur.key, !cur.public, activeId(st)))
|
|
1096
|
+
await toggleVisibility(term, st, cur.public, () => vc.setSecretVisibility(cur.ns, cur.key, !cur.public, activeId(st), activePwd(st)))
|
|
1020
1097
|
} else if (key.name === 'f5') {
|
|
1021
1098
|
await refreshSecrets(term, st)
|
|
1022
1099
|
}
|
|
@@ -1027,6 +1104,38 @@ async function onKeySecrets (term, st, key) {
|
|
|
1027
1104
|
* Hacer pública una variable es dejar que su valor SALGA de esta máquina, así que se
|
|
1028
1105
|
* pregunta; volverla privada no expone nada y se aplica directo.
|
|
1029
1106
|
*/
|
|
1107
|
+
/**
|
|
1108
|
+
* VER el valor de una variable. Es lo único que la contraseña guarda desde v5, y en la
|
|
1109
|
+
* máquina de la bóveda no hay otro camino: la llave de este aparato vive en este mismo
|
|
1110
|
+
* disco, así que si abriera sin frase una copia del disco abriría todo.
|
|
1111
|
+
*
|
|
1112
|
+
* Si el perfil no tiene contraseña se abre con la llave de la máquina y no se pregunta
|
|
1113
|
+
* nada — pero se dice, para que no parezca una protección que no está puesta.
|
|
1114
|
+
*/
|
|
1115
|
+
async function revealValue (term, st, owner, key, isPublic) {
|
|
1116
|
+
const i = L(st)
|
|
1117
|
+
const p = activeProfile(st)
|
|
1118
|
+
const mostrar = async (pwd) => {
|
|
1119
|
+
const r = await guard(term, st, i.revealing, () => vc.revealSecret(owner, key, activeId(st), pwd))
|
|
1120
|
+
if (r.ok) flash(st, i.revealed(key, r.v), 'ok')
|
|
1121
|
+
}
|
|
1122
|
+
if (isPublic) return mostrar(undefined) // una pública no está cerrada
|
|
1123
|
+
if (!p?.protected) { flash(st, i.revealNoPwd, 'warn'); return mostrar(undefined) }
|
|
1124
|
+
const guardada = activePwd(st)
|
|
1125
|
+
if (guardada) return mostrar(guardada)
|
|
1126
|
+
setInput(st, {
|
|
1127
|
+
label: i.revealAsk,
|
|
1128
|
+
hint: i.revealHint,
|
|
1129
|
+
mask: true,
|
|
1130
|
+
onSubmit: async (pwd) => {
|
|
1131
|
+
st.input = null
|
|
1132
|
+
if (!pwd) return
|
|
1133
|
+
await mostrar(pwd)
|
|
1134
|
+
},
|
|
1135
|
+
onCancel: () => { st.input = null }
|
|
1136
|
+
})
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1030
1139
|
async function toggleVisibility (term, st, wasPublic, apply) {
|
|
1031
1140
|
const i = L(st)
|
|
1032
1141
|
const run = async () => {
|
|
@@ -1059,7 +1168,9 @@ async function onKeyDevVars (term, st, key) {
|
|
|
1059
1168
|
if (ch === 'n') { promptNewDeviceVariable(term, st); return true }
|
|
1060
1169
|
if (ch === 'i' && target) { promptLoadVars(term, st, { pub: target.pub, where: target.deviceId }); return true }
|
|
1061
1170
|
if (ch === 't' && cur && target) {
|
|
1062
|
-
await toggleVisibility(term, st, cur.public, () => vc.setDeviceSecretVisibility(target.pub, cur.key, !cur.public, activeId(st)))
|
|
1171
|
+
await toggleVisibility(term, st, cur.public, () => vc.setDeviceSecretVisibility(target.pub, cur.key, !cur.public, activeId(st), activePwd(st)))
|
|
1172
|
+
} else if (ch === 'v' && cur?.key) {
|
|
1173
|
+
await revealValue(term, st, `dev:${target.pub}`, cur.key, cur.public)
|
|
1063
1174
|
return true
|
|
1064
1175
|
}
|
|
1065
1176
|
if ((ch === 'x' || key.name === 'delete') && cur && target) {
|
|
@@ -1355,7 +1466,10 @@ function render (term, st) {
|
|
|
1355
1466
|
const ap = activeProfile(st)
|
|
1356
1467
|
const apTxt = ap ? `${t.accent('●')} ${t.bold(ap.name || i.noName)} ${lockGlyph(ap)} ${t.muted('· ' + (ap.fingerprint || '—'))}` : t.muted('—')
|
|
1357
1468
|
lines[1] = ' ' + i.activeVault + apTxt
|
|
1358
|
-
|
|
1469
|
+
// El .deb instala el binario pero NO reinicia el servicio (y si se reinicia ANTES de
|
|
1470
|
+
// instalar, peor: el daemon se queda con el binario viejo, ya borrado, y hay dos copias
|
|
1471
|
+
// en RAM). `status` ya lo avisa; aquí también, que es donde uno se queda mirando.
|
|
1472
|
+
lines[2] = (up && s?.version && VERSION !== 'dev' && s.version !== VERSION) ? ' ' + t.warn(i.daemonStale(s.version, VERSION)) : ''
|
|
1359
1473
|
// Dispositivos/Scopes son pestañas de la bóveda activa (se entra desde Bóvedas);
|
|
1360
1474
|
// el resto muestra su título simple.
|
|
1361
1475
|
lines[3] = INNER_TABS.includes(st.screen) ? ' ' + renderTabs(st, t) : ' ' + t.title('» ' + title(i, st.screen))
|
package/src/tui/i18n.js
CHANGED
|
@@ -26,6 +26,7 @@ const es = {
|
|
|
26
26
|
// encabezado / estado
|
|
27
27
|
daemonRunning: 'corriendo',
|
|
28
28
|
daemonStopped: 'DETENIDO',
|
|
29
|
+
daemonStale: (running, installed) => `⚠ el servicio corre ${running} y el binario instalado es ${installed}: reinícialo (systemctl --user restart dotrino-vault)`,
|
|
29
30
|
activeVault: 'Bóveda activa: ',
|
|
30
31
|
noName: '(sin nombre)',
|
|
31
32
|
tooSmall: 'Terminal muy pequeño',
|
|
@@ -83,6 +84,8 @@ const es = {
|
|
|
83
84
|
|
|
84
85
|
// bóvedas (perfiles)
|
|
85
86
|
noPassword: 'sin clave',
|
|
87
|
+
noPasswordWarn: 'Este perfil no tiene contraseña: una copia de este disco abre las variables privadas.',
|
|
88
|
+
pendingSeal: (owner, kind, who) => kind === 'rotate' ? `${owner}: sin rotar (salió un aparato). Guarda una variable con la contraseña.` : `${owner}: sin llave todavía en ${who}. Se reparte sola con otro aparato del grupo encendido, o al abrir la bóveda.`,
|
|
86
89
|
locked: '🔒 bloqueada',
|
|
87
90
|
unlocked: '🔓 abierta',
|
|
88
91
|
passwordOf: (name) => `Contraseña de "${name}"`,
|
|
@@ -109,7 +112,7 @@ const es = {
|
|
|
109
112
|
deletingVault: 'Borrando bóveda…',
|
|
110
113
|
vaultDeleted: 'Bóveda borrada',
|
|
111
114
|
newPasswordLabel: (name) => `Contraseña nueva para "${name}" (mín. 4)`,
|
|
112
|
-
passwordTooShort: 'La contraseña debe tener al menos
|
|
115
|
+
passwordTooShort: 'La contraseña debe tener al menos 12 caracteres: usa varias palabras al azar',
|
|
113
116
|
repeatPassword: 'Repite la contraseña',
|
|
114
117
|
passwordMismatch: 'Las contraseñas no coinciden',
|
|
115
118
|
savingPassword: 'Guardando contraseña…',
|
|
@@ -129,6 +132,7 @@ const es = {
|
|
|
129
132
|
noLabel: '(sin etiqueta)',
|
|
130
133
|
// Está en el acta y no puede entrar: o le retiraron el certificado, o se le venció.
|
|
131
134
|
deviceNoAccess: 'SIN ACCESO — está en el acta pero no puede entrar',
|
|
135
|
+
deviceDebt: (n) => `no abre ${n}`,
|
|
132
136
|
thisVault: 'esta bóveda (manda ella)',
|
|
133
137
|
cantRemoveMaster: 'Esta bóveda es la que manda: no se quita a sí misma.',
|
|
134
138
|
revokedCount: (n) => ` Revocados: ${n}`,
|
|
@@ -218,6 +222,9 @@ const es = {
|
|
|
218
222
|
newAccountLabel: 'Nombre de la cuenta nueva',
|
|
219
223
|
newAccountHint: 'nace vacía; el dispositivo será su primer invitado',
|
|
220
224
|
accountCreated: (name) => `Cuenta creada: ${name}`,
|
|
225
|
+
discardingAccount: 'Descartando la cuenta…',
|
|
226
|
+
accountDiscarded: 'Cuenta descartada: nadie llegó a entrar en ella',
|
|
227
|
+
confirmDiscardAccount: 'La cuenta se creó para este emparejamiento y quedó vacía. ¿La descarto?',
|
|
221
228
|
// Cuenta + vigencia en UNA línea: cada línea de cabecera es una fila menos de QR
|
|
222
229
|
// visible antes de tener que hacer scroll.
|
|
223
230
|
pairAccount: (name, min) => `Cuenta que se comparte: ${name} · válido ~${min} min`,
|
|
@@ -257,14 +264,21 @@ const es = {
|
|
|
257
264
|
deviceRenamed: (n) => `Ahora se llama «${n}»`,
|
|
258
265
|
helpSecrets: ({ hasSecrets } = {}) => [
|
|
259
266
|
'←→ pestaña', '↑↓', 'n nueva variable', 'i cargar varias',
|
|
260
|
-
...(hasSecrets ? ['t pública/privada', 'x quitar (variable/scope)'] : []),
|
|
267
|
+
...(hasSecrets ? ['v ver valor', 't pública/privada', 'x quitar (variable/scope)'] : []),
|
|
261
268
|
'F5 refrescar', 'Esc bóvedas', 'l English', 'q salir'
|
|
262
269
|
],
|
|
263
270
|
helpDevVars: ({ hasVars } = {}) => [
|
|
264
271
|
'↑↓', 'n nueva variable', 'i cargar varias',
|
|
265
|
-
...(hasVars ? ['t pública/privada', 'x quitar'] : []),
|
|
272
|
+
...(hasVars ? ['v ver valor', 't pública/privada', 'x quitar'] : []),
|
|
266
273
|
'F5 refrescar', 'Esc dispositivos', 'l English', 'q salir'
|
|
267
274
|
],
|
|
275
|
+
// Ver el valor de una privada: lo único que la contraseña guarda en esta máquina.
|
|
276
|
+
revealTitle: 'Ver el valor',
|
|
277
|
+
revealAsk: 'Contraseña del perfil (para ver el valor)',
|
|
278
|
+
revealHint: 'sin ella no se puede abrir: es lo único que la guarda (Esc cancela)',
|
|
279
|
+
revealing: 'abriendo…',
|
|
280
|
+
revealed: (k, v) => `${k} = ${v}`,
|
|
281
|
+
revealNoPwd: 'Este perfil no tiene contraseña: se abre con la llave de esta máquina.',
|
|
268
282
|
helpPairing: ['a aprobar', 'x rechazar', 'r reiniciar', '↑↓ scroll', 'Esc atrás', 'l English'],
|
|
269
283
|
helpPairMode: ['↑↓', 'Enter elegir', 'Esc atrás', 'l English', 'q salir'],
|
|
270
284
|
|
|
@@ -311,6 +325,7 @@ const en = {
|
|
|
311
325
|
|
|
312
326
|
daemonRunning: 'running',
|
|
313
327
|
daemonStopped: 'STOPPED',
|
|
328
|
+
daemonStale: (running, installed) => `⚠ the service runs ${running} but the installed binary is ${installed}: restart it (systemctl --user restart dotrino-vault)`,
|
|
314
329
|
activeVault: 'Active vault: ',
|
|
315
330
|
tooSmall: 'Terminal too small',
|
|
316
331
|
tooSmallHint: (cols, rows) => `Resize to ≥ 24×9 (now ${cols}×${rows}).`,
|
|
@@ -363,6 +378,8 @@ const en = {
|
|
|
363
378
|
titlePairMode: 'Pairing: which account does it join?',
|
|
364
379
|
|
|
365
380
|
noPassword: 'no password',
|
|
381
|
+
noPasswordWarn: 'This profile has no password: a copy of this disk opens the private variables.',
|
|
382
|
+
pendingSeal: (owner, kind, who) => kind === 'rotate' ? `${owner}: not rotated (a device left). Save a variable with the password.` : `${owner}: no key yet on ${who}. It is handed out by another device of the group that is on, or when the vault is opened.`,
|
|
366
383
|
locked: '🔒 locked',
|
|
367
384
|
unlocked: '🔓 unlocked',
|
|
368
385
|
passwordOf: (name) => `Password for "${name}"`,
|
|
@@ -389,7 +406,7 @@ const en = {
|
|
|
389
406
|
deletingVault: 'Deleting vault…',
|
|
390
407
|
vaultDeleted: 'Vault deleted',
|
|
391
408
|
newPasswordLabel: (name) => `New password for "${name}" (min. 4)`,
|
|
392
|
-
passwordTooShort: 'The password must be at least
|
|
409
|
+
passwordTooShort: 'The password must be at least 12 characters: use several random words',
|
|
393
410
|
repeatPassword: 'Repeat the password',
|
|
394
411
|
passwordMismatch: 'The passwords do not match',
|
|
395
412
|
savingPassword: 'Saving password…',
|
|
@@ -407,6 +424,7 @@ const en = {
|
|
|
407
424
|
noDevices: ' (no devices enrolled — press P to pair one)',
|
|
408
425
|
noLabel: '(no label)',
|
|
409
426
|
deviceNoAccess: 'NO ACCESS — it is on the record but cannot get in',
|
|
427
|
+
deviceDebt: (n) => `cannot open ${n}`,
|
|
410
428
|
thisVault: 'this vault (it is the Master)',
|
|
411
429
|
cantRemoveMaster: 'This vault is the Master: it does not remove itself.',
|
|
412
430
|
revokedCount: (n) => ` Revoked: ${n}`,
|
|
@@ -491,6 +509,9 @@ const en = {
|
|
|
491
509
|
newAccountLabel: 'Name of the new account',
|
|
492
510
|
newAccountHint: 'born empty; the device will be its first guest',
|
|
493
511
|
accountCreated: (name) => `Account created: ${name}`,
|
|
512
|
+
discardingAccount: 'Discarding the account…',
|
|
513
|
+
accountDiscarded: 'Account discarded: nobody got to join it',
|
|
514
|
+
confirmDiscardAccount: 'The account was created for this pairing and is empty. Discard it?',
|
|
494
515
|
pairAccount: (name, min) => `Account being shared: ${name} · valid ~${min} min`,
|
|
495
516
|
pairScan: 'Scan it, or open this address on the device:',
|
|
496
517
|
pairUrl: 'URL: ',
|
|
@@ -518,14 +539,20 @@ const en = {
|
|
|
518
539
|
deviceRenamed: (n) => `Now called "${n}"`,
|
|
519
540
|
helpSecrets: ({ hasSecrets } = {}) => [
|
|
520
541
|
'←→ tab', '↑↓', 'n new variable', 'i load several',
|
|
521
|
-
...(hasSecrets ? ['t public/private', 'x remove (variable/scope)'] : []),
|
|
542
|
+
...(hasSecrets ? ['v show value', 't public/private', 'x remove (variable/scope)'] : []),
|
|
522
543
|
'F5 refresh', 'Esc vaults', 'l Español', 'q quit'
|
|
523
544
|
],
|
|
524
545
|
helpDevVars: ({ hasVars } = {}) => [
|
|
525
546
|
'↑↓', 'n new variable', 'i load several',
|
|
526
|
-
...(hasVars ? ['t public/private', 'x remove'] : []),
|
|
547
|
+
...(hasVars ? ['v show value', 't public/private', 'x remove'] : []),
|
|
527
548
|
'F5 refresh', 'Esc devices', 'l Español', 'q quit'
|
|
528
549
|
],
|
|
550
|
+
revealTitle: 'Show the value',
|
|
551
|
+
revealAsk: 'Profile password (to show the value)',
|
|
552
|
+
revealHint: 'without it there is no way to open it: it is the only thing guarding it (Esc cancels)',
|
|
553
|
+
revealing: 'opening…',
|
|
554
|
+
revealed: (k, v) => `${k} = ${v}`,
|
|
555
|
+
revealNoPwd: 'This profile has no password: it opens with this machine key.',
|
|
529
556
|
helpPairing: ['a approve', 'x reject', 'r restart', '↑↓ scroll', 'Esc back', 'l Español'],
|
|
530
557
|
helpPairMode: ['↑↓', 'Enter choose', 'Esc back', 'l Español', 'q quit'],
|
|
531
558
|
|