@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.
- package/lib/src/admin.js +29 -3
- package/lib/src/index.js +4 -1
- package/lib/src/localdesk.js +96 -0
- package/lib/src/service.js +95 -18
- package/package.json +7 -3
- package/src/approvals.js +7 -2
- package/src/client.js +4 -1
- package/src/ctl.js +19 -2
- package/src/daemon.js +4 -0
- package/src/replica.js +34 -1
- package/src/secretsStore.js +136 -0
- package/src/threadStore.js +10 -0
- package/src/transport.js +27 -4
- package/src/tui/app.js +42 -2
- package/src/tui/i18n.js +52 -35
- package/src/vault.js +211 -14
- package/src/vaultControl.js +24 -0
package/src/secretsStore.js
CHANGED
|
@@ -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ó —
|
package/src/threadStore.js
CHANGED
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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:
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
|
74
|
+
store: 'escribir en tu bóveda: perfil, contenido, datos',
|
|
70
75
|
read: 'ver lo que guardaste',
|
|
71
|
-
admin: '
|
|
72
|
-
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',
|
|
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
|
|
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: '
|
|
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) => `
|
|
381
|
-
sign: '
|
|
382
|
-
store: '
|
|
383
|
-
read: '
|
|
384
|
-
admin: '
|
|
385
|
-
passwords: '
|
|
386
|
-
approve: '
|
|
387
|
-
sealer: '
|
|
388
|
-
unattended: '
|
|
389
|
-
replica: '
|
|
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
|
|
406
|
+
store: 'write to your vault: profile, content, data',
|
|
394
407
|
read: 'see what you saved',
|
|
395
|
-
admin: '
|
|
396
|
-
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',
|
|
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
|
|
400
|
-
replica: '
|
|
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 } = {}) => [
|