@dotrino/vaultd 0.98.0 → 0.105.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.
@@ -72,6 +72,32 @@ const KEY_RE = /^[A-Z0-9_]{1,64}$/
72
72
  */
73
73
  export const RECOVERY = '#recovery'
74
74
 
75
+ /**
76
+ * EL CAJÓN DEL PERFIL. Los datos de tu identidad se guardan como cualquier variable —
77
+ * mismo sobre, mismo llavero, mismo histórico— porque son lo mismo: un valor que solo
78
+ * pueden leer tus aparatos. Diseño: `docs/datos-del-perfil.md`.
79
+ *
80
+ * SE LLAMA `@me` A PROPÓSITO, y es la garantía entera: un nombre de cajón válido es
81
+ * `[a-z0-9-]{1,32}`, así que **ningún servicio puede llamarse `@me`**. Por lo tanto
82
+ * `memberCanReadSecrets(acta, quien, '@me')` no puede ser cierto nunca y no hay forma de
83
+ * pedir el perfil por el camino de los secretos. No es una comprobación que alguien pueda
84
+ * olvidarse de hacer: es un nombre que el validador no deja existir.
85
+ */
86
+ export const PROFILE_NS = '@me'
87
+ export const PROFILE_OWNER = 'ns:' + PROFILE_NS
88
+
89
+ /**
90
+ * El nombre de un dato del perfil. NO vale `assertVar`: aquélla exige MAYÚSCULAS porque
91
+ * describe una variable de entorno, y aquí los campos son `nickname`, `telefono`,
92
+ * `apellidos`. Mismo espíritu —un nombre corto y previsible, sin sorpresas al serializar—
93
+ * y distinto alfabeto.
94
+ */
95
+ export function assertProfileKey (key) {
96
+ if (typeof key !== 'string' || !/^[a-z][a-z0-9_]{0,31}$/.test(key)) {
97
+ throw new Error('invalid profile field (use lowercase_with_underscores, e.g. "nickname")')
98
+ }
99
+ }
100
+
75
101
  /** Cuántas versiones anteriores se conservan, en total. Ver «EL HISTÓRICO» arriba. */
76
102
  const MAX_HISTORY = 500
77
103
 
@@ -818,6 +844,75 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
818
844
  owners () {
819
845
  return [...Object.keys(data.ns).map((k) => `ns:${k}`), ...Object.keys(data.dev).map((k) => `dev:${k}`)]
820
846
  },
847
+ // ---------------- EL PERFIL (`docs/datos-del-perfil.md`) ----------------
848
+ //
849
+ // Escribir un dato PRIVADO no necesita nada de aquí: es `putSealed(PROFILE_OWNER, …)`,
850
+ // que ya guarda un sobre hecho sin abrirlo y funciona con la bóveda cerrada. Lo que
851
+ // falta es leerlo, y el camino de lo PÚBLICO, que no se parece a ninguna variable.
852
+
853
+ /**
854
+ * Los sobres del perfil TALLADOS a un aparato: sus entradas y las envolturas suyas.
855
+ * Igual que `bundleFor`, sin `assertNs` — `@me` no es un nombre de cajón válido, y esa
856
+ * es justamente su garantía.
857
+ *
858
+ * El PAQUETE LO ARMA EL APARATO (dueño, 2026-09-03): aquí solo se entrega lo que hay.
859
+ */
860
+ profileBundleFor (devicePub) {
861
+ const bag = data.ns
862
+ const k = PROFILE_NS
863
+ if (isLegacy() || !bag[k]) return { entries: {}, wraps: [] }
864
+ const wraps = (bag[k].keyring || [])
865
+ .filter((g) => g.wraps?.[devicePub])
866
+ .map((g) => ({ gen: g.gen, wrap: g.wraps[devicePub] }))
867
+ return { entries: { ...varsOf(bag, k) }, wraps }
868
+ },
869
+
870
+ /**
871
+ * LO PÚBLICO DE VERDAD: en claro, y firmado. Es lo que recibe quien pregunta desde
872
+ * fuera y no tiene ninguna llave tuya — no hay a quién sellárselo (dueño, 2026-09-03).
873
+ *
874
+ * Va firmado igual que lo demás: un dato público sin firma es un dato que cualquiera
875
+ * puede inventar en tu nombre, que es peor que no tenerlo.
876
+ */
877
+ profilePublic () {
878
+ const bag = data.ns[PROFILE_NS]
879
+ if (!bag?.vars) return {}
880
+ const out = {}
881
+ for (const [key, e] of Object.entries(bag.vars)) {
882
+ if (e?.cls !== 'public' || typeof e.pubv !== 'string') continue
883
+ out[key] = { v: e.pubv, gen: e.gen, seal: e.seal || null, at: e.at || null, by: e.by || null }
884
+ }
885
+ return out
886
+ },
887
+
888
+ /**
889
+ * Guarda un dato PÚBLICO del perfil: en claro y con su firma. No lleva sobre ni
890
+ * envoltura porque no se le sella a nadie.
891
+ *
892
+ * `pubv` y no `v`: un `v` suelto dentro de un paquete de variables es un error duro
893
+ * desde el 2026-09-02 (`plaintext-var`), y con razón. Aquí es lo correcto, así que se
894
+ * escribe con otro nombre — el paquete del perfil lo arma otro código y esa invariante
895
+ * de los secretos se queda intacta.
896
+ */
897
+ async profilePutPublic (key, value, { by = null } = {}) {
898
+ return enFila(async () => {
899
+ if (isLegacy()) throw new Error('profile: this store is still v3 (run `migrate` first)')
900
+ if (needsMigration()) throw new NeedsMigration()
901
+ assertProfileKey(key)
902
+ if (typeof value !== 'string' || value.length > 200000) {
903
+ throw new Error('a public profile field must be a string of at most 200 kB')
904
+ }
905
+ const vars = ensureBag(data.ns, PROFILE_NS)
906
+ const before = vars[key]
907
+ const gen = (topGen(data.ns, PROFILE_NS)?.gen || 0) + 1
908
+ const seal = signer ? await signer({ owner: PROFILE_OWNER, key, gen, iv: '', ct: value }) : null
909
+ pushHistory(PROFILE_OWNER, key, before, by)
910
+ vars[key] = { cls: 'public', pub: true, owner: PROFILE_OWNER, gen, pubv: value, seal, at: Date.now(), by: by || null }
911
+ save()
912
+ return { gen }
913
+ })
914
+ },
915
+
821
916
  /** @private */
822
917
  /**
823
918
  * @param {Array|((isPublic:boolean)=>Promise<Array>)} members A quién envolver. Puede ser
@@ -895,6 +990,47 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
895
990
  return { wrapped, sealed, sinLlave: [...sinLlave] }
896
991
  },
897
992
 
993
+ /**
994
+ * RENOMBRAR UNA VARIABLE. Cambia el nombre y NADA MÁS: el mismo sobre, la misma
995
+ * generación, las mismas envolturas. El valor no se toca ni se mira.
996
+ *
997
+ * Lo único que hay que rehacer es la FIRMA, porque el nombre va dentro de lo firmado
998
+ * (§8.8: `{owner, key, gen, iv, ct}`). Sin eso el sobre quedaría diciendo que se llama
999
+ * de otra manera y el agente lo rechazaría al abrirlo — un fallo que aparecería lejos
1000
+ * de aquí y sin pista.
1001
+ *
1002
+ * Y firmar de nuevo NO pide la frase: la llave de sellado existe justo para esto, no
1003
+ * abre nada. Así que renombrar funciona con la bóveda cerrada, como escribir.
1004
+ *
1005
+ * El HISTÓRICO se muda con ella: es la misma variable, y dejarlo bajo el nombre viejo
1006
+ * lo volvería inalcanzable — se vería como un hueco, no como una decisión.
1007
+ */
1008
+ async rename (owner, from, to) { return enFila(() => this._renameRaw(owner, from, to)) },
1009
+ /** @private */
1010
+ async _renameRaw (owner, from, to) {
1011
+ if (needsMigration()) throw new NeedsMigration()
1012
+ assertKeyValue(to, 'x')
1013
+ const [kind, k] = splitOwner(owner)
1014
+ const bag = kind === 'ns' ? data.ns : data.dev
1015
+ const vars = varsOf(bag, k)
1016
+ const e = vars[from]
1017
+ if (!e) throw new Error(`there is no variable called "${from}" in ${owner}`)
1018
+ if (from === to) return { renamed: false }
1019
+ if (vars[to]) throw new Error(`"${to}" already exists in ${owner}: pick another name or remove it first`)
1020
+
1021
+ // La firma lleva el nombre dentro, así que se rehace. Si esta bóveda no puede firmar,
1022
+ // se para: dejar el sobre con la firma vieja es dejarlo roto para quien lo abra.
1023
+ if (e.e) {
1024
+ if (!signer) throw new Error('this vault cannot sign right now: renaming would leave the envelope signed with the old name')
1025
+ e.seal = await signer({ owner, key: to, gen: e.gen, iv: e.e.iv, ct: e.e.ct })
1026
+ }
1027
+ delete vars[from]
1028
+ vars[to] = e
1029
+ for (const h of data.history) if (h.owner === owner && h.key === from) h.key = to
1030
+ save()
1031
+ return { renamed: true }
1032
+ },
1033
+
898
1034
  /**
899
1035
  * ROTA de verdad: vuelve a cifrar las variables privadas del cajón con llaves nuevas
900
1036
  * y solo para los miembros dados. Es lo que corta el acceso de quien salió —
@@ -103,6 +103,16 @@ export function openThreadStore (dir) {
103
103
  },
104
104
  profileGet () { return { me: data.profile || null } },
105
105
 
106
+ // LOS DATOS DEL PERFIL EN SOBRES (`docs/datos-del-perfil.md`). Están aquí solo para
107
+ // que el filtro de métodos los reconozca: los atiende `handleProfile` en `vault.js`,
108
+ // antes de llegar a este store, porque su puerta es otra —`firma` y sin candado—.
109
+ // Si alguna vez alguien los llamara por este camino, contestan que no en vez de
110
+ // hacer algo a medias.
111
+ profilePut () { throw new Error('profilePut is handled before the store: see handleProfile') },
112
+ profileBundle () { throw new Error('profileBundle is handled before the store: see handleProfile') },
113
+ profilePublic () { throw new Error('profilePublic is handled before the store: see handleProfile') },
114
+ profileRecipients () { throw new Error('profileRecipients is handled before the store: see handleProfile') },
115
+
106
116
  // ----- DATOS SENSIBLES del usuario (F4, docs/consola-remota.md §6) -----
107
117
  //
108
118
  // Contraseñas, notas, documentos: van al contenido del perfil, cifrados con la
package/src/transport.js CHANGED
@@ -46,11 +46,33 @@ export async function createTransport ({ identity, dir, url = DEFAULT_PROXY, com
46
46
  // su `identify` pisaba al anterior. Sigue siendo el cliente oficial del paquete.
47
47
  const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
48
48
 
49
- // WebRTC off: el vault usa el proxy como transporte (RTCPeerConnection no existe
50
- // en Node). Reconexión prácticamente ilimitada: un daemon de larga duración no
51
- // debe rendirse tras unos intentos.
49
+ /**
50
+ * EL CANAL DIRECTO, ENCENDIDO Y CON LA PUERTA CERRADA.
51
+ *
52
+ * Encendido porque es el escalón 2 de la regla del transporte (`CLAUDE.md`) y en Node ya
53
+ * hay con qué (`werift`, que el pilar carga solo). Si no lo hubiera, negociar falla y se
54
+ * sigue por el proxio — el escalón 4 siempre está.
55
+ *
56
+ * Y con la puerta cerrada desde el primer día, que es lo que hace defendible encenderlo:
57
+ * **solo se le negocia a quien está en el acta.** Aceptar una señal arranca DTLS, ICE y
58
+ * SCTP, o sea código que parsea red no confiable, dentro del proceso que guarda la
59
+ * maestra. Quien decide si eso corre no puede ser el que llama a la puerta.
60
+ *
61
+ * Reconexión prácticamente ilimitada: un daemon de larga duración no debe rendirse tras
62
+ * unos intentos.
63
+ */
64
+ // La puerta tiene que decidir AL INSTANTE (la señal llega y o se atiende o no), y pedir
65
+ // el acta es asíncrono. Así que se guarda una copia y se refresca donde ya se refresca
66
+ // todo: en `identify`, que es lo que corre cada vez que el acta cambia o se reconecta.
67
+ let actaVista = null
68
+ const enElActa = (token) => {
69
+ // SIN ACTA NO SE NEGOCIA. No se sabe quién es nadie, y en la duda no se arranca a
70
+ // parsear red no confiable. Se sigue por el proxio, que es el escalón que siempre está.
71
+ if (!actaVista) return false
72
+ return (actaVista.members || []).some((m) => m?.pub === token)
73
+ }
52
74
  const client = new WebSocketProxyClient({
53
- url, enableWebRTC: false, autoReconnect: true,
75
+ url, enableWebRTC: true, acceptDirectFrom: enElActa, autoReconnect: true,
54
76
  maxReconnectAttempts: 100000, reconnectDelay: 4000
55
77
  })
56
78
 
@@ -59,6 +81,7 @@ export async function createTransport ({ identity, dir, url = DEFAULT_PROXY, com
59
81
  const identify = async () => {
60
82
  if (!client.token) return
61
83
  const record = (await identity.profileActa?.().catch(() => null))?.acta || null
84
+ actaVista = record // la copia que mira la puerta del canal directo
62
85
  const comm = commKey?.pub?.() || null
63
86
  // ¿Nos nombra el acta? Con `cn` o sin él, lo que el proxio mira es la pertenencia.
64
87
  const esMiembro = comm && (record?.members || []).some((m) => m?.pub === comm)
package/src/tui/app.js CHANGED
@@ -390,6 +390,8 @@ function capsRows (st, t) {
390
390
  // qué vas a firmar — que es justo lo que hace peligroso acumular cambios.
391
391
  const cambia = has.has(cap) !== real.has(cap)
392
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.
393
395
  const line = ` ${mark}${cambia ? t.bold('*') : ' '} ${cap === 'admin' ? t.bold(name) : name}`
394
396
  rows.push({ text: line, sel: true, meta: { cap } })
395
397
  const hint = typeof i.capHint[cap] === 'function' ? i.capHint[cap](member.cn) : i.capHint[cap]
@@ -1312,12 +1314,44 @@ async function onKeySecrets (term, st, key) {
1312
1314
  await revealValue(term, st, `ns:${cur.ns}`, cur.key, cur.public)
1313
1315
  } else if (ch === 't' && cur?.key) {
1314
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)
1315
1319
  } else if (key.name === 'f5') {
1316
1320
  await refreshSecrets(term, st)
1317
1321
  }
1318
1322
  return true
1319
1323
  }
1320
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
+
1321
1355
  /**
1322
1356
  * Hacer pública una variable es dejar que su valor SALGA de esta máquina, así que se
1323
1357
  * pregunta; volverla privada no expone nada y se aplica directo.
@@ -1420,11 +1454,17 @@ async function onKeyDevVars (term, st, key) {
1420
1454
  */
1421
1455
  function askVisibility (term, st, done) {
1422
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.
1423
1463
  setConfirm(st, {
1424
1464
  text: i.newVarPrivateAsk,
1425
1465
  defaultYes: true,
1426
- onYes: () => { st.confirm = null; done(false) }, // privada
1427
- onNo: () => { st.confirm = null; done(true) } // pública
1466
+ onYes: () => { st.confirm = null; return done(false) }, // privada
1467
+ onNo: () => { st.confirm = null; return done(true) } // pública
1428
1468
  })
1429
1469
  }
1430
1470
 
package/src/tui/i18n.js CHANGED
@@ -49,36 +49,41 @@ const es = {
49
49
  // lo que hacen y no lo que implican»). «Sellar el acta» y «Administrar el perfil» eran
50
50
  // lo segundo: hay que saberse el modelo para traducirlos. Lo que hacen es admitir
51
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.
52
56
  capName: {
53
- // Lleva el cajón dentro: «las claves de proxy» dice QUÉ abre; «lee sus claves» te deja
54
- // preguntándote cuáles. Solo se le ofrece a un servicio (los que tienen `cn`).
55
- secrets: (cn) => `Leer las claves de «${cn}»`,
56
- sign: 'Entrar a las apps como tú',
57
- store: 'Guardar tus datos',
58
- read: 'Leer tus datos',
59
- admin: 'Conectar y quitar dispositivos',
60
- passwords: 'Pedir tus contraseñas',
61
- approve: 'Autorizar a otros aparatos',
62
- sealer: 'Admitir aparatos sin esta bóveda',
63
- unattended: 'Llevarse claves sin pedirte permiso',
64
- replica: 'Entregar tus claves con la bóveda apagada'
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'
65
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.
66
71
  capHint: {
67
- 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`,
68
73
  sign: 'usar tu identidad en las apps del ecosistema',
69
- store: 'escribir en tu bóveda (perfil, contenido, datos sensibles)',
74
+ store: 'escribir en tu bóveda: perfil, contenido, datos',
70
75
  read: 'ver lo que guardaste',
71
- admin: 'hacerlo desde ese aparato, sin venir aquí. No puede cambiar permisos',
72
- 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',
73
78
  approve: 'dar el visto bueno cuando otro aparato pide claves privadas',
74
79
  // Sellar solo significa algo en un aparato que HAGA de bóveda (otra máquina con el
75
80
  // daemon, o un navegador que se puso de bóveda). En un teléfono normal se concede y no
76
81
  // lo usa nadie. Se enseña igual, y se dice: verlo desactivado explica el modelo;
77
82
  // esconderlo hacía creer que no existe (dueño, 2026-08-31).
78
83
  sealer: 'admitir aparatos y cambiar permisos él solo cuando esta bóveda no está. Solo sirve en otra BÓVEDA',
79
- unattended: 'llevarse claves privadas SIN preguntarte. Apagado, cada vez que arranque tienes que aprobarlo desde un aparato con «Aprobar»',
84
+ unattended: 'llevarse claves privadas sin preguntarte. Apagado, hay que aprobarlo desde un aparato con «aprueba» cada vez que arranque',
80
85
  // El replicador reparte y no puede nada más: no abre lo que entrega ni cambia nada.
81
- replica: 'las entrega cerradas, tal como salieron de aquí: no puede abrirlas ni cambiar nada. Solo sirve en otra máquina'
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'
82
87
  },
83
88
  confirmAdmin: (id) => `¿Dejar que ${id} conecte y quite dispositivos sin venir aquí?`,
84
89
  capGiven: (n) => `Concedido: ${n}`,
@@ -227,6 +232,10 @@ const es = {
227
232
  newVarPrivateAsk: '¿Es privada?',
228
233
  makePublicConfirm: '¿Hacerla pública?',
229
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`,
230
239
  nowPublic: 'Pública: se entrega sin pedirte permiso',
231
240
  nowPrivate: 'Privada: para entregarla te pide permiso',
232
241
 
@@ -299,7 +308,7 @@ const es = {
299
308
  deviceRenamed: (n) => `Ahora se llama «${n}»`,
300
309
  helpSecrets: ({ hasSecrets } = {}) => [
301
310
  '←→ pestaña', '↑↓', 'n nueva variable', 'i cargar varias',
302
- ...(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)'] : []),
303
312
  'F5 refrescar', 'Esc bóvedas', 'l English', 'q salir'
304
313
  ],
305
314
  helpDevVars: ({ hasVars } = {}) => [
@@ -376,28 +385,32 @@ const en = {
376
385
  capsSaved: (n) => `${n} change(s) saved in one go`,
377
386
  capsNothing: 'You have not changed anything',
378
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.
379
390
  capName: {
380
- secrets: (cn) => `Read the «${cn}» keys`,
381
- sign: 'Sign in to apps as you',
382
- store: 'Save your data',
383
- read: 'Read your data',
384
- admin: 'Connect and remove devices',
385
- passwords: 'Ask for your passwords',
386
- approve: 'Authorise other devices',
387
- sealer: 'Admit devices without this vault',
388
- unattended: 'Take keys without asking you',
389
- replica: 'Hand out your keys while the vault is off'
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'
390
401
  },
402
+ // Cada pista se sostiene sola: el título es la palabra, no una frase que continuar.
391
403
  capHint: {
404
+ secrets: (cn) => `open the «${cn}» drawer and no other`,
392
405
  sign: 'use your identity across the ecosystem apps',
393
- store: 'write to your vault (profile, content, sensitive data)',
406
+ store: 'write to your vault: profile, content, data',
394
407
  read: 'see what you saved',
395
- admin: 'do it from that device, without coming here. It cannot change permissions',
396
- 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',
397
410
  approve: 'give the go-ahead when another device asks for private keys',
398
411
  sealer: 'admit devices and change permissions on its own when this vault is away. Only useful on another VAULT',
399
- unattended: 'take private keys WITHOUT asking you. Off, you must approve it from a device with «Approve» every time it starts',
400
- replica: 'it hands them over sealed, exactly as they left here: it cannot open them or change anything. Only useful on another machine'
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'
401
414
  },
402
415
  confirmAdmin: (id) => `Let ${id} connect and remove devices without coming here?`,
403
416
  capGiven: (n) => `Granted: ${n}`,
@@ -535,6 +548,10 @@ const en = {
535
548
  newVarPrivateAsk: 'Is it private?',
536
549
  makePublicConfirm: 'Make it public?',
537
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`,
538
555
  nowPublic: 'Public: handed over without asking you',
539
556
  nowPrivate: 'Private: handing it over asks you first',
540
557
 
@@ -593,7 +610,7 @@ const en = {
593
610
  deviceRenamed: (n) => `Now called "${n}"`,
594
611
  helpSecrets: ({ hasSecrets } = {}) => [
595
612
  '←→ tab', '↑↓', 'n new variable', 'i load several',
596
- ...(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)'] : []),
597
614
  'F5 refresh', 'Esc vaults', 'l Español', 'q quit'
598
615
  ],
599
616
  helpDevVars: ({ hasVars } = {}) => [