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