@dotrino/vaultd 0.97.0 → 0.105.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/lib/src/admin.js +29 -3
- package/lib/src/enroll.js +12 -1
- package/lib/src/index.js +4 -1
- package/lib/src/localdesk.js +96 -0
- package/lib/src/service.js +185 -34
- package/package.json +6 -3
- package/src/approvals.js +7 -2
- package/src/client.js +4 -1
- package/src/ctl.js +105 -3
- package/src/daemon.js +17 -1
- package/src/replica.js +232 -0
- package/src/secretsStore.js +136 -0
- package/src/threadStore.js +10 -0
- package/src/transport.js +27 -4
- package/src/tui/app.js +76 -9
- package/src/tui/i18n.js +85 -55
- package/src/vault.js +298 -15
- package/src/vaultControl.js +24 -0
package/src/tui/app.js
CHANGED
|
@@ -37,6 +37,7 @@ import { qrToString } from '../qr.js'
|
|
|
37
37
|
import { dict, otherLang, loadLang, saveLang } from './i18n.js'
|
|
38
38
|
import * as vc from '../vaultControl.js'
|
|
39
39
|
import { VERSION } from '../version.js'
|
|
40
|
+
import { DEVICE_CAPS } from '@dotrino/identity/acta'
|
|
40
41
|
|
|
41
42
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
42
43
|
|
|
@@ -338,7 +339,18 @@ function pairModeRows (st, t) {
|
|
|
338
339
|
* permiso que hay que entender para un servidor: dice si esa máquina se lleva tus claves
|
|
339
340
|
* privadas sola o si te lo pregunta. Verlo apagado es la mitad del mensaje.
|
|
340
341
|
*/
|
|
341
|
-
|
|
342
|
+
/**
|
|
343
|
+
* EL ORDEN LO ELIGE ESTA PANTALLA; LA LISTA LA MANDA EL ACTA.
|
|
344
|
+
*
|
|
345
|
+
* Antes esto era una lista suelta escrita a mano, y por eso cada permiso nuevo se quedó
|
|
346
|
+
* invisible aquí durante semanas —`sealer`, `unattended` y `secrets`, tres veces el mismo
|
|
347
|
+
* fallo—. Ahora el orden se cura a mano, que es una decisión de diseño, pero lo que no
|
|
348
|
+
* esté nombrado se añade al final: un permiso puede salir en mal sitio, nunca desaparecer.
|
|
349
|
+
* `test/tui-permisos.test.mjs` exige además que todos tengan nombre en los dos idiomas.
|
|
350
|
+
*/
|
|
351
|
+
const ORDEN = ['sign', 'store', 'read', 'admin', 'approve', 'passwords', 'sealer', 'unattended', 'replica']
|
|
352
|
+
const CAPS_ORDER = [...ORDEN.filter((c) => DEVICE_CAPS.includes(c)),
|
|
353
|
+
...DEVICE_CAPS.filter((c) => !ORDEN.includes(c))]
|
|
342
354
|
|
|
343
355
|
/**
|
|
344
356
|
* Los permisos que se le pueden dar A ESTE miembro, en orden.
|
|
@@ -378,6 +390,8 @@ function capsRows (st, t) {
|
|
|
378
390
|
// qué vas a firmar — que es justo lo que hace peligroso acumular cambios.
|
|
379
391
|
const cambia = has.has(cap) !== real.has(cap)
|
|
380
392
|
if (cambia) tocados++
|
|
393
|
+
// El título ES el nombre del permiso —el mismo que se teclea en `caps <ID> +administra`—
|
|
394
|
+
// así que no hay que repetirlo al lado. Lo que hace va debajo.
|
|
381
395
|
const line = ` ${mark}${cambia ? t.bold('*') : ' '} ${cap === 'admin' ? t.bold(name) : name}`
|
|
382
396
|
rows.push({ text: line, sel: true, meta: { cap } })
|
|
383
397
|
const hint = typeof i.capHint[cap] === 'function' ? i.capHint[cap](member.cn) : i.capHint[cap]
|
|
@@ -1300,12 +1314,44 @@ async function onKeySecrets (term, st, key) {
|
|
|
1300
1314
|
await revealValue(term, st, `ns:${cur.ns}`, cur.key, cur.public)
|
|
1301
1315
|
} else if (ch === 't' && cur?.key) {
|
|
1302
1316
|
await toggleVisibility(term, st, cur.public, () => vc.setSecretVisibility(cur.ns, cur.key, !cur.public, activeId(st), activePwd(st)))
|
|
1317
|
+
} else if (ch === 'r' && cur?.key) {
|
|
1318
|
+
await promptRenameVariable(term, st, cur)
|
|
1303
1319
|
} else if (key.name === 'f5') {
|
|
1304
1320
|
await refreshSecrets(term, st)
|
|
1305
1321
|
}
|
|
1306
1322
|
return true
|
|
1307
1323
|
}
|
|
1308
1324
|
|
|
1325
|
+
/**
|
|
1326
|
+
* RENOMBRAR: el mismo sobre con otro nombre. No pide la frase, porque lo único que hay que
|
|
1327
|
+
* rehacer es la firma —el nombre va DENTRO de lo firmado— y de eso se encarga la llave de
|
|
1328
|
+
* sellado, que trabaja con la bóveda cerrada.
|
|
1329
|
+
*
|
|
1330
|
+
* Se avisa de lo que de verdad importa y no se ve: para el servicio que la lee esto es un
|
|
1331
|
+
* cambio de configuración, así que se va a reiniciar. Si su código busca el nombre viejo,
|
|
1332
|
+
* dejará de encontrarlo — y eso no lo puede saber la bóveda.
|
|
1333
|
+
*/
|
|
1334
|
+
function promptRenameVariable (term, st, cur) {
|
|
1335
|
+
const i = L(st)
|
|
1336
|
+
return new Promise((listo) => {
|
|
1337
|
+
setInput(st, {
|
|
1338
|
+
label: i.renameLabel(cur.key),
|
|
1339
|
+
hint: i.renameHint,
|
|
1340
|
+
value: cur.key,
|
|
1341
|
+
onSubmit: async (nuevo) => {
|
|
1342
|
+
const nv = nuevo.trim()
|
|
1343
|
+
st.input = null
|
|
1344
|
+
if (!nv || nv === cur.key) { listo(); return }
|
|
1345
|
+
if (!KEY_RE.test(nv)) { flash(st, i.keyInvalid, 'danger'); listo(); return }
|
|
1346
|
+
const r = await guard(term, st, i.renaming, () => vc.renameSecret(cur.ns, cur.key, nv, activeId(st)))
|
|
1347
|
+
if (r.ok) { flash(st, i.renamed(cur.key, nv)); st.secrets = r.v }
|
|
1348
|
+
listo()
|
|
1349
|
+
},
|
|
1350
|
+
onCancel: () => { st.input = null; listo() }
|
|
1351
|
+
})
|
|
1352
|
+
})
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1309
1355
|
/**
|
|
1310
1356
|
* Hacer pública una variable es dejar que su valor SALGA de esta máquina, así que se
|
|
1311
1357
|
* pregunta; volverla privada no expone nada y se aplica directo.
|
|
@@ -1349,6 +1395,8 @@ async function toggleVisibility (term, st, wasPublic, apply) {
|
|
|
1349
1395
|
if (r.ok) { flash(st, wasPublic ? i.nowPrivate : i.nowPublic); st.secrets = r.v }
|
|
1350
1396
|
}
|
|
1351
1397
|
if (wasPublic) return run()
|
|
1398
|
+
// Solo se pregunta al ABRIRLA. Volverla privada es la dirección segura y se hace sin
|
|
1399
|
+
// confirmar: pedir permiso para cerrar algo solo entrena a decir que sí.
|
|
1352
1400
|
setConfirm(st, { text: i.makePublicConfirm, onYes: run, onNo: () => { st.confirm = null } })
|
|
1353
1401
|
}
|
|
1354
1402
|
|
|
@@ -1394,17 +1442,29 @@ async function onKeyDevVars (term, st, key) {
|
|
|
1394
1442
|
}
|
|
1395
1443
|
|
|
1396
1444
|
/**
|
|
1397
|
-
* Al crear una variable se
|
|
1398
|
-
*
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1445
|
+
* Al crear una variable se pregunta SI ES PRIVADA. Se pregunta al crearla, y no después,
|
|
1446
|
+
* porque es cuando quien la escribe sabe qué es: un puerto se puede enseñar, una llave de
|
|
1447
|
+
* producción no.
|
|
1448
|
+
*
|
|
1449
|
+
* OJO AL SENTIDO. Antes se preguntaba lo contrario («¿que se pueda ver?») y el defecto
|
|
1450
|
+
* —Enter o Esc— caía en `onNo`, que era la privada, o sea la segura. Al dar la vuelta a la
|
|
1451
|
+
* pregunta, ese mismo defecto pasaría a ser la PÚBLICA sin que nadie lo notara: cada
|
|
1452
|
+
* variable nueva se entregaría sin pedir permiso. Por eso el defecto se declara aquí
|
|
1453
|
+
* (`defaultYes`) en vez de heredarse del manejador de teclas.
|
|
1401
1454
|
*/
|
|
1402
1455
|
function askVisibility (term, st, done) {
|
|
1403
1456
|
const i = L(st)
|
|
1457
|
+
// SE DEVUELVE LA PROMESA. `onConfirmKey` hace `await f?.()` para SERIALIZAR las
|
|
1458
|
+
// operaciones contra el daemon —lo dice el comentario de `onInputKey`—, y soltarla aquí
|
|
1459
|
+
// dejaba la escritura corriendo por fuera de esa serialización: se solapaba con el
|
|
1460
|
+
// refresco periódico, los dos usan el mismo archivo de respuesta, el refresco lo borraba
|
|
1461
|
+
// antes de que la escritura lo leyera y la pantalla se quedaba en «Guardando…» para
|
|
1462
|
+
// siempre. El valor SÍ se había guardado, que es lo que lo hacía tan confuso.
|
|
1404
1463
|
setConfirm(st, {
|
|
1405
|
-
text: i.
|
|
1406
|
-
|
|
1407
|
-
|
|
1464
|
+
text: i.newVarPrivateAsk,
|
|
1465
|
+
defaultYes: true,
|
|
1466
|
+
onYes: () => { st.confirm = null; return done(false) }, // privada
|
|
1467
|
+
onNo: () => { st.confirm = null; return done(true) } // pública
|
|
1408
1468
|
})
|
|
1409
1469
|
}
|
|
1410
1470
|
|
|
@@ -1554,7 +1614,14 @@ async function onConfirmKey (st, key) {
|
|
|
1554
1614
|
// La tecla es `y` (yes) en los dos idiomas —como el resto, mnemónico inglés—;
|
|
1555
1615
|
// `s` (sí) se sigue aceptando por costumbre, pero no se anuncia en la ayuda.
|
|
1556
1616
|
if (ch === 's' || ch === 'y') { const f = cf.onYes; st.confirm = null; await f?.() }
|
|
1557
|
-
else if (ch === 'n'
|
|
1617
|
+
else if (ch === 'n') { const f = cf.onNo; st.confirm = null; await f?.() }
|
|
1618
|
+
// Enter y Esc toman el CAMINO SEGURO, y cuál es lo dice quien pregunta. Sin esto el
|
|
1619
|
+
// defecto era siempre «no», y una pregunta formulada al revés lo volvía el peligroso.
|
|
1620
|
+
else if (key.name === 'escape' || key.name === 'enter' || key.name === 'ctrl-c') {
|
|
1621
|
+
const f = cf.defaultYes ? cf.onYes : cf.onNo
|
|
1622
|
+
st.confirm = null
|
|
1623
|
+
await f?.()
|
|
1624
|
+
}
|
|
1558
1625
|
}
|
|
1559
1626
|
|
|
1560
1627
|
// --------------------------------- render ----------------------------------
|
package/src/tui/i18n.js
CHANGED
|
@@ -39,45 +39,57 @@ const es = {
|
|
|
39
39
|
// PERMISOS de un dispositivo (§9.1: se dice el beneficio, no el scope del cert).
|
|
40
40
|
titleCaps: 'Permisos del dispositivo',
|
|
41
41
|
capsFor: (id, name) => `Permisos de ${id}${name ? ' · ' + name : ''}`,
|
|
42
|
-
capsNoMember: ' (este dispositivo ya no está en el
|
|
42
|
+
capsNoMember: ' (este dispositivo ya no está en el perfil)',
|
|
43
43
|
capsApplyHint: 'Enter marca o desmarca · G guarda los cambios · Esc sale',
|
|
44
|
-
capsPending: (n) => `${n} cambio(s) sin guardar (*) · G para guardarlos
|
|
45
|
-
capsSaved: (n) => `${n} cambio(s) guardados
|
|
44
|
+
capsPending: (n) => `${n} cambio(s) sin guardar (*) · G para guardarlos todos de una vez · Esc para descartarlos`,
|
|
45
|
+
capsSaved: (n) => `${n} cambio(s) guardados de una vez`,
|
|
46
46
|
capsNothing: 'No has cambiado nada',
|
|
47
47
|
capsDiscard: '¿Descartar los cambios sin guardar?',
|
|
48
|
+
// EL NOMBRE DICE EL ACTO, NO LA CONSECUENCIA (dueño, 2026-09-02: «que digan exactamente
|
|
49
|
+
// lo que hacen y no lo que implican»). «Sellar el acta» y «Administrar el perfil» eran
|
|
50
|
+
// lo segundo: hay que saberse el modelo para traducirlos. Lo que hacen es admitir
|
|
51
|
+
// aparatos y conectar o quitar dispositivos, y eso es lo que se lee ahora.
|
|
52
|
+
// EL TÍTULO ES EL NOMBRE DEL PERMISO, no una descripción (dueño, 2026-09-03: «deben
|
|
53
|
+
// decir siempre lo que SON»). Antes puse frases —«Conectar y quitar dispositivos»— y el
|
|
54
|
+
// resultado fue una lista donde no se sabía qué permiso era cuál ni cómo nombrarlo para
|
|
55
|
+
// el CLI. Lo que hace cada uno va DEBAJO, que es su sitio.
|
|
48
56
|
capName: {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
unattended: 'Recibir claves sin aprobación'
|
|
57
|
+
secrets: (cn) => `secretos «${cn}»`,
|
|
58
|
+
sign: 'firma',
|
|
59
|
+
store: 'guarda',
|
|
60
|
+
read: 'lee',
|
|
61
|
+
admin: 'administra',
|
|
62
|
+
passwords: 'contrasenas',
|
|
63
|
+
approve: 'aprueba',
|
|
64
|
+
sealer: 'sella',
|
|
65
|
+
unattended: 'desatendido',
|
|
66
|
+
replica: 'replica'
|
|
60
67
|
},
|
|
68
|
+
// CADA PISTA SE SOSTIENE SOLA. Antes colgaban del título —«hacerlo desde ese aparato»
|
|
69
|
+
// continuaba a «Conectar y quitar dispositivos»—; ahora el título es la palabra que se
|
|
70
|
+
// teclea, así que la pista tiene que decir la frase entera.
|
|
61
71
|
capHint: {
|
|
62
|
-
secrets: (cn) => `abrir
|
|
72
|
+
secrets: (cn) => `abrir el cajón de «${cn}» y ningún otro`,
|
|
63
73
|
sign: 'usar tu identidad en las apps del ecosistema',
|
|
64
|
-
store: 'escribir en tu bóveda
|
|
74
|
+
store: 'escribir en tu bóveda: perfil, contenido, datos',
|
|
65
75
|
read: 'ver lo que guardaste',
|
|
66
|
-
admin: 'conectar y quitar
|
|
67
|
-
passwords: '
|
|
76
|
+
admin: 'conectar y quitar aparatos desde ahí, sin venir aquí. No puede cambiar permisos',
|
|
77
|
+
passwords: 'pedir una contraseña por sitio, nunca la lista entera',
|
|
68
78
|
approve: 'dar el visto bueno cuando otro aparato pide claves privadas',
|
|
69
79
|
// Sellar solo significa algo en un aparato que HAGA de bóveda (otra máquina con el
|
|
70
80
|
// daemon, o un navegador que se puso de bóveda). En un teléfono normal se concede y no
|
|
71
81
|
// lo usa nadie. Se enseña igual, y se dice: verlo desactivado explica el modelo;
|
|
72
82
|
// esconderlo hacía creer que no existe (dueño, 2026-08-31).
|
|
73
|
-
sealer: 'admitir aparatos y cambiar permisos él solo
|
|
74
|
-
unattended: 'llevarse claves privadas
|
|
83
|
+
sealer: 'admitir aparatos y cambiar permisos él solo cuando esta bóveda no está. Solo sirve en otra BÓVEDA',
|
|
84
|
+
unattended: 'llevarse claves privadas sin preguntarte. Apagado, hay que aprobarlo desde un aparato con «aprueba» cada vez que arranque',
|
|
85
|
+
// El replicador reparte y no puede nada más: no abre lo que entrega ni cambia nada.
|
|
86
|
+
replica: 'repartir los sobres que esta bóveda ya firmó, tal como salieron: no puede abrirlos ni cambiar nada. Solo sirve en otra máquina'
|
|
75
87
|
},
|
|
76
88
|
confirmAdmin: (id) => `¿Dejar que ${id} conecte y quite dispositivos sin venir aquí?`,
|
|
77
89
|
capGiven: (n) => `Concedido: ${n}`,
|
|
78
90
|
capTaken: (n) => `Quitado: ${n}`,
|
|
79
91
|
applyingCaps: 'Aplicando…',
|
|
80
|
-
loadingMembers: 'Cargando
|
|
92
|
+
loadingMembers: 'Cargando los dispositivos…',
|
|
81
93
|
helpCaps: ['↑↓', 'Enter marcar', 'G guardar', 'F5 refrescar', 'Esc dispositivos', 'l English', 'q salir'],
|
|
82
94
|
// Perfil del usuario (lo que sincronizan los dispositivos). Solo lectura: se edita en
|
|
83
95
|
// el aparato, no en la máquina donde vive la bóveda.
|
|
@@ -152,7 +164,7 @@ const es = {
|
|
|
152
164
|
noDevices: ' (sin dispositivos enrolados — pulsa P para emparejar uno)',
|
|
153
165
|
noLabel: '(sin etiqueta)',
|
|
154
166
|
// Está en el acta y no puede entrar: o le retiraron el certificado, o se le venció.
|
|
155
|
-
deviceNoAccess: 'SIN ACCESO — está en el
|
|
167
|
+
deviceNoAccess: 'SIN ACCESO — está en el perfil pero no puede entrar',
|
|
156
168
|
deviceDebt: (n) => `no abre ${n}`,
|
|
157
169
|
thisVault: 'esta bóveda (manda ella)',
|
|
158
170
|
cantRemoveMaster: 'Esta bóveda es la que manda: no se quita a sí misma.',
|
|
@@ -165,7 +177,7 @@ const es = {
|
|
|
165
177
|
noPendingToReject: 'No hay ningún dispositivo pendiente para rechazar',
|
|
166
178
|
rejecting: 'Rechazando…',
|
|
167
179
|
deviceRejected: 'Dispositivo rechazado',
|
|
168
|
-
revokeConfirm: (id) => `¿Quitar ${id} del perfil?
|
|
180
|
+
revokeConfirm: (id) => `¿Quitar ${id} del perfil? Pierde el acceso ahora mismo. Lo que tenga guardado se le borra cuando se conecte: si está apagado, sigue ahí hasta entonces.`,
|
|
169
181
|
revoking: 'Revocando…',
|
|
170
182
|
deviceRevoked: (id) => `Revocado ${id}`,
|
|
171
183
|
approveLabel: (id) => `Código que MUESTRA el dispositivo ${id}`,
|
|
@@ -213,11 +225,19 @@ const es = {
|
|
|
213
225
|
},
|
|
214
226
|
loadNothing: 'No se cargó nada:',
|
|
215
227
|
varPublic: 'pública',
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
228
|
+
// LA PREGUNTA ES LA PREGUNTA, sin explicar la consecuencia (dueño, 2026-09-02:
|
|
229
|
+
// «debe decir: ¿la variable es privada? y punto»). Antes decía «¿que su valor se pueda
|
|
230
|
+
// VER desde la consola remota?», que además ya era FALSO: desde el 2026-09-02 las dos se
|
|
231
|
+
// guardan cifradas igual y `pública` solo dice que se entrega sin pedirte permiso.
|
|
232
|
+
newVarPrivateAsk: '¿Es privada?',
|
|
233
|
+
makePublicConfirm: '¿Hacerla pública?',
|
|
234
|
+
changingVisibility: 'Cambiando…',
|
|
235
|
+
renameLabel: (k) => `Nuevo nombre para ${k}`,
|
|
236
|
+
renameHint: 'MAYUSCULAS_CON_GUION_BAJO · el valor no se toca · Esc cancela',
|
|
237
|
+
renaming: 'Renombrando…',
|
|
238
|
+
renamed: (a, b) => `${a} ahora se llama ${b} — el servicio que la lee se reinicia; si busca el nombre viejo, ya no lo encuentra`,
|
|
239
|
+
nowPublic: 'Pública: se entrega sin pedirte permiso',
|
|
240
|
+
nowPrivate: 'Privada: para entregarla te pide permiso',
|
|
221
241
|
|
|
222
242
|
// variables de UN aparato (Dispositivos → tecla e). Las del scope las comparten todos
|
|
223
243
|
// los aparatos que sirven ese namespace; estas las lee solo él, y le ganan.
|
|
@@ -288,7 +308,7 @@ const es = {
|
|
|
288
308
|
deviceRenamed: (n) => `Ahora se llama «${n}»`,
|
|
289
309
|
helpSecrets: ({ hasSecrets } = {}) => [
|
|
290
310
|
'←→ pestaña', '↑↓', 'n nueva variable', 'i cargar varias',
|
|
291
|
-
...(hasSecrets ? ['v ver valor', 't pública/privada', 'x quitar (variable/scope)'] : []),
|
|
311
|
+
...(hasSecrets ? ['v ver valor', 't pública/privada', 'r renombrar', 'x quitar (variable/scope)'] : []),
|
|
292
312
|
'F5 refrescar', 'Esc bóvedas', 'l English', 'q salir'
|
|
293
313
|
],
|
|
294
314
|
helpDevVars: ({ hasVars } = {}) => [
|
|
@@ -332,11 +352,11 @@ const es = {
|
|
|
332
352
|
// El código NO es la contraseña de la bóveda: son los seis dígitos que el aparato
|
|
333
353
|
// enseña en su pantalla. Si no coincide, la bóveda no firma nada y el aparato sigue
|
|
334
354
|
// esperando ahí, así que se puede volver a intentar con A.
|
|
335
|
-
errWrongCode: 'El código no coincide con el que muestra el dispositivo: no
|
|
355
|
+
errWrongCode: 'El código no coincide con el que muestra el dispositivo: no ha entrado nadie. Míralo otra vez y pulsa A.',
|
|
336
356
|
errProfileLocked: 'Bóveda bloqueada: ábrela con su contraseña',
|
|
337
357
|
errWrongPassword: (n) => `Contraseña incorrecta${n ? ` — van ${n} intentos fallidos` : ''}`,
|
|
338
358
|
errTooManyTries: (s) => `Demasiados intentos: espera ${s || '?'} s antes de volver a probar`,
|
|
339
|
-
errMasterWithMembers: 'Esta bóveda es
|
|
359
|
+
errMasterWithMembers: 'Esta bóveda es la que manda en esta cuenta y hay otros dispositivos: pásale primero el mando a uno que esté conectado. Si la borras así, se quedan con su llave y sin nadie que pueda volver a darles permisos.'
|
|
340
360
|
}
|
|
341
361
|
|
|
342
362
|
// ---------------------------------- inglés ----------------------------------
|
|
@@ -362,35 +382,41 @@ const en = {
|
|
|
362
382
|
capsNoMember: ' (this device is no longer in the record)',
|
|
363
383
|
capsApplyHint: 'Enter ticks or unticks · G saves · Esc leaves',
|
|
364
384
|
capsPending: (n) => `${n} unsaved change(s) (*) · G to save them in ONE record · Esc to discard`,
|
|
365
|
-
capsSaved: (n) => `${n} change(s) saved in
|
|
385
|
+
capsSaved: (n) => `${n} change(s) saved in one go`,
|
|
366
386
|
capsNothing: 'You have not changed anything',
|
|
367
387
|
capsDiscard: 'Discard the unsaved changes?',
|
|
388
|
+
// El título es el NOMBRE del permiso, el mismo que se teclea. Ver la nota en el bloque
|
|
389
|
+
// español: describirlo dejaba una lista donde no se sabía qué permiso era cuál.
|
|
368
390
|
capName: {
|
|
369
|
-
secrets: (cn) => `
|
|
370
|
-
sign: '
|
|
371
|
-
store: '
|
|
372
|
-
read: '
|
|
373
|
-
admin: '
|
|
374
|
-
passwords: '
|
|
375
|
-
approve: '
|
|
376
|
-
sealer: '
|
|
377
|
-
unattended: '
|
|
391
|
+
secrets: (cn) => `secrets «${cn}»`,
|
|
392
|
+
sign: 'sign',
|
|
393
|
+
store: 'store',
|
|
394
|
+
read: 'read',
|
|
395
|
+
admin: 'admin',
|
|
396
|
+
passwords: 'passwords',
|
|
397
|
+
approve: 'approve',
|
|
398
|
+
sealer: 'sealer',
|
|
399
|
+
unattended: 'unattended',
|
|
400
|
+
replica: 'replica'
|
|
378
401
|
},
|
|
402
|
+
// Cada pista se sostiene sola: el título es la palabra, no una frase que continuar.
|
|
379
403
|
capHint: {
|
|
404
|
+
secrets: (cn) => `open the «${cn}» drawer and no other`,
|
|
380
405
|
sign: 'use your identity across the ecosystem apps',
|
|
381
|
-
store: 'write to your vault
|
|
406
|
+
store: 'write to your vault: profile, content, data',
|
|
382
407
|
read: 'see what you saved',
|
|
383
|
-
admin: 'connect and remove devices from
|
|
384
|
-
passwords: '
|
|
408
|
+
admin: 'connect and remove devices from there, without coming here. It cannot change permissions',
|
|
409
|
+
passwords: 'ask for one password per site, never the whole list',
|
|
385
410
|
approve: 'give the go-ahead when another device asks for private keys',
|
|
386
|
-
sealer: 'admit devices and change permissions on its own
|
|
387
|
-
unattended: 'take private keys
|
|
411
|
+
sealer: 'admit devices and change permissions on its own when this vault is away. Only useful on another VAULT',
|
|
412
|
+
unattended: 'take private keys without asking you. Off, it must be approved from a device with «approve» every time it starts',
|
|
413
|
+
replica: 'hand out the envelopes this vault already signed, exactly as they left: it cannot open them or change anything. Only useful on another machine'
|
|
388
414
|
},
|
|
389
415
|
confirmAdmin: (id) => `Let ${id} connect and remove devices without coming here?`,
|
|
390
416
|
capGiven: (n) => `Granted: ${n}`,
|
|
391
417
|
capTaken: (n) => `Removed: ${n}`,
|
|
392
418
|
applyingCaps: 'Applying…',
|
|
393
|
-
loadingMembers: 'Loading the
|
|
419
|
+
loadingMembers: 'Loading the devices…',
|
|
394
420
|
helpCaps: ['↑↓', 'Enter tick', 'G save', 'F5 refresh', 'Esc devices', 'l Español', 'q quit'],
|
|
395
421
|
loadingProfile: 'Loading profile…',
|
|
396
422
|
noProfile: ' (this vault has no profile yet)',
|
|
@@ -474,7 +500,7 @@ const en = {
|
|
|
474
500
|
noPendingToReject: 'No device is waiting to be rejected',
|
|
475
501
|
rejecting: 'Rejecting…',
|
|
476
502
|
deviceRejected: 'Device rejected',
|
|
477
|
-
revokeConfirm: (id) => `Remove ${id} from the profile? It
|
|
503
|
+
revokeConfirm: (id) => `Remove ${id} from the profile? It loses access right away. What it has stored is erased when it connects: if it is switched off, it stays there until then.`,
|
|
478
504
|
revoking: 'Revoking…',
|
|
479
505
|
deviceRevoked: (id) => `Revoked ${id}`,
|
|
480
506
|
approveLabel: (id) => `Code SHOWN by device ${id}`,
|
|
@@ -519,11 +545,15 @@ const en = {
|
|
|
519
545
|
},
|
|
520
546
|
loadNothing: 'Nothing was loaded:',
|
|
521
547
|
varPublic: 'public',
|
|
522
|
-
|
|
523
|
-
makePublicConfirm: '
|
|
524
|
-
changingVisibility: 'Changing
|
|
525
|
-
|
|
526
|
-
|
|
548
|
+
newVarPrivateAsk: 'Is it private?',
|
|
549
|
+
makePublicConfirm: 'Make it public?',
|
|
550
|
+
changingVisibility: 'Changing…',
|
|
551
|
+
renameLabel: (k) => `New name for ${k}`,
|
|
552
|
+
renameHint: 'UPPERCASE_WITH_UNDERSCORES · the value is not touched · Esc cancels',
|
|
553
|
+
renaming: 'Renaming…',
|
|
554
|
+
renamed: (a, b) => `${a} is now ${b} — the service that reads it restarts; if it looks for the old name, it will not find it`,
|
|
555
|
+
nowPublic: 'Public: handed over without asking you',
|
|
556
|
+
nowPrivate: 'Private: handing it over asks you first',
|
|
527
557
|
|
|
528
558
|
// variables de UN aparato (Dispositivos → tecla e)
|
|
529
559
|
titleDevVars: 'Device variables',
|
|
@@ -580,7 +610,7 @@ const en = {
|
|
|
580
610
|
deviceRenamed: (n) => `Now called "${n}"`,
|
|
581
611
|
helpSecrets: ({ hasSecrets } = {}) => [
|
|
582
612
|
'←→ tab', '↑↓', 'n new variable', 'i load several',
|
|
583
|
-
...(hasSecrets ? ['v show value', 't public/private', 'x remove (variable/scope)'] : []),
|
|
613
|
+
...(hasSecrets ? ['v show value', 't public/private', 'r rename', 'x remove (variable/scope)'] : []),
|
|
584
614
|
'F5 refresh', 'Esc vaults', 'l Español', 'q quit'
|
|
585
615
|
],
|
|
586
616
|
helpDevVars: ({ hasVars } = {}) => [
|
|
@@ -618,7 +648,7 @@ const en = {
|
|
|
618
648
|
errNotApplied: 'The daemon did not apply the change (check the service logs).',
|
|
619
649
|
errNotDeleted: 'The daemon did not delete the variable (check the service logs).',
|
|
620
650
|
errPairFailed: 'The daemon did not start the pairing.',
|
|
621
|
-
errWrongCode: 'The code does not match the one shown by the device:
|
|
651
|
+
errWrongCode: 'The code does not match the one shown by the device: nobody got in. Check it and press A again.',
|
|
622
652
|
errProfileLocked: 'Vault locked: open it with its password',
|
|
623
653
|
errWrongPassword: (n) => `Wrong password${n ? ` — ${n} failed attempts so far` : ''}`,
|
|
624
654
|
errTooManyTries: (s) => `Too many attempts: wait ${s || '?'} s before trying again`,
|