@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/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
- const CAPS_ORDER = ['sign', 'store', 'read', 'admin', 'approve', 'passwords', 'sealer', 'unattended']
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 PREGUNTA si su valor puede salir de esta máquina. Se pregunta
1398
- * al crearla, y no después, porque es cuando quien la escribe sabe qué es: un puerto se
1399
- * puede enseñar, una llave de producción no. La respuesta por defecto —Enter, o `n`— es
1400
- * la privada.
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.newVarPublicAsk,
1406
- onYes: () => { st.confirm = null; done(true) },
1407
- onNo: () => { st.confirm = null; done(false) }
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' || key.name === 'escape' || key.name === 'enter' || key.name === 'ctrl-c') { const f = cf.onNo; st.confirm = null; await f?.() }
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 acta)',
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 en UNA sola acta · Esc para descartarlos`,
45
- capsSaved: (n) => `${n} cambio(s) guardados en una sola acta`,
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
- // 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}»`,
52
- sign: 'Firmar como tú',
53
- store: 'Guardar tus datos',
54
- read: 'Leer tus datos',
55
- admin: 'Administrar el perfil',
56
- passwords: 'Pedir tus contraseñas',
57
- approve: 'Aprobar pedidos',
58
- sealer: 'Sellar el acta',
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 SU cajón y ninguno más: las variables de «${cn}»`,
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 (perfil, contenido, datos sensibles)',
74
+ store: 'escribir en tu bóveda: perfil, contenido, datos',
65
75
  read: 'ver lo que guardaste',
66
- admin: 'conectar y quitar dispositivos desde ese aparato, sin venir aquí',
67
- passwords: 'el gestor: pedir una credencial por sitio, nunca la lista entera',
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, si esta bóveda no está. Solo sirve en otra BÓVEDA',
74
- unattended: 'llevarse claves privadas SIN preguntarte. Apagado, cada vez que arranque tienes que aprobarlo desde un aparato con «Aprobar»'
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 el acta…',
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 acta pero no puede entrar',
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? Sale del acta y se le retiran los certificados ya. Lo que tenga guardado se le borra cuando se conecte: si está apagado, sigue ahí hasta entonces.`,
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
- newVarPublicAsk: '¿Que su valor se pueda VER desde la consola remota? (las demás no salen de esta máquina)',
217
- makePublicConfirm: '¿Dejar que su valor se vea desde la consola remota?',
218
- changingVisibility: 'Cambiando visibilidad…',
219
- nowPublic: 'Ahora su valor se puede ver desde la consola remota',
220
- nowPrivate: 'Ahora su valor no sale de esta máquina',
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 se emitió ningún certificado. Míralo otra vez y pulsa A.',
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 el Master de esta cuenta y hay otros dispositivos: pásale primero el Master a uno que esté conectado. Si la borras así, se quedan con su llave y sin nadie que pueda volver a firmar el acta.'
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 a single record`,
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) => `Read the «${cn}» keys`,
370
- sign: 'Sign as you',
371
- store: 'Save your data',
372
- read: 'Read your data',
373
- admin: 'Manage the profile',
374
- passwords: 'Ask for your passwords',
375
- approve: 'Approve requests',
376
- sealer: 'Seal the record',
377
- unattended: 'Take keys without approval'
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 (profile, content, sensitive data)',
406
+ store: 'write to your vault: profile, content, data',
382
407
  read: 'see what you saved',
383
- admin: 'connect and remove devices from that device, without coming here',
384
- passwords: 'the manager: ask for one credential per site, never the whole list',
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 if this vault is gone. Only useful on another VAULT',
387
- unattended: 'take private keys WITHOUT asking you. Off, you must approve it from a device with «Approve» every time it starts'
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 record…',
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 leaves the record and its certificates are withdrawn right away. What it has stored is erased when it connects: if it is switched off, it stays there until then.`,
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
- newVarPublicAsk: 'Let its value be SEEN from the remote console? (the rest never leave this machine)',
523
- makePublicConfirm: 'Let its value be seen from the remote console?',
524
- changingVisibility: 'Changing visibility…',
525
- nowPublic: 'Its value can now be seen from the remote console',
526
- nowPrivate: 'Its value no longer leaves this machine',
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: no certificate was issued. Check it and press A again.',
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`,