@dotrino/vaultd 0.130.0 → 0.131.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/package.json +2 -2
- package/src/ctl.js +8 -1
- package/src/daemon.js +45 -9
- package/src/secretsStore.js +18 -0
- package/src/tui/app.js +5 -0
- package/src/tui/i18n.js +4 -0
- package/src/vault.js +2 -0
- package/src/vaultControl.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.131.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Certificador personal de Dotrino: daemon headless que custodia la clave maestra y delega capacidades a tus dispositivos por el proxy. Tu CA propia.",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@dotrino/proxy-client": "0.24.0",
|
|
29
29
|
"@dotrino/roadmap": "0.3.7",
|
|
30
30
|
"@dotrino/store": "0.11.0",
|
|
31
|
-
"@dotrino/update": "0.2.
|
|
31
|
+
"@dotrino/update": "0.2.2",
|
|
32
32
|
"ws": "^8.18.0"
|
|
33
33
|
},
|
|
34
34
|
"license": "MIT",
|
package/src/ctl.js
CHANGED
|
@@ -520,7 +520,9 @@ async function cmdMe () {
|
|
|
520
520
|
// El volcado es contenido del usuario: se lee y se BORRA, no se queda ahí suelto.
|
|
521
521
|
try { fs.rmSync(meFile, { force: true }) } catch (_) {}
|
|
522
522
|
if (!dump) { console.error('La bóveda no respondió. ¿Está corriendo? dotrino-vault status'); process.exit(1) }
|
|
523
|
-
|
|
523
|
+
// Con el perfil CERRADO se enseña igual lo público (la bóveda ya se lo da a cualquiera);
|
|
524
|
+
// lo que no sale son ni los nombres de los datos privados. Se dice.
|
|
525
|
+
if (dump.locked && !dump.me) assertOpen(dump)
|
|
524
526
|
|
|
525
527
|
const me = dump.me
|
|
526
528
|
if (!me) {
|
|
@@ -530,6 +532,7 @@ async function cmdMe () {
|
|
|
530
532
|
}
|
|
531
533
|
|
|
532
534
|
const when = me.updatedAt ? new Date(me.updatedAt).toLocaleString() : '—'
|
|
535
|
+
if (dump.locked) console.log('\n(perfil bloqueado: solo lo público; ábrelo con dotrino-vault unlock para ver qué datos privados hay)')
|
|
533
536
|
console.log('\n%sPerfil%s · actualizado %s\n', B, Z, when)
|
|
534
537
|
console.log(' nombre : %s', me.nickname || '(sin nombre)')
|
|
535
538
|
console.log(' foto : %s', me.avatar
|
|
@@ -546,6 +549,10 @@ async function cmdMe () {
|
|
|
546
549
|
console.log(' %s: %s%s', label.padEnd(12), me[k], me[k + 'Visible'] === false ? ' (oculto)' : '')
|
|
547
550
|
}
|
|
548
551
|
}
|
|
552
|
+
// Lo PRIVADO viaja sellado y la bóveda no lo abre para enseñarlo: se dice qué hay.
|
|
553
|
+
if (Array.isArray(me.privateKeys) && me.privateKeys.length) {
|
|
554
|
+
console.log('\n privados : %s (sellados: se ven en tus aparatos, no aquí)', me.privateKeys.join(', '))
|
|
555
|
+
}
|
|
549
556
|
for (const [title, list] of [['Enlaces', me.links], ['Otros datos', me.fields]]) {
|
|
550
557
|
if (!Array.isArray(list) || !list.length) continue
|
|
551
558
|
console.log('\n %s:', title)
|
package/src/daemon.js
CHANGED
|
@@ -342,6 +342,26 @@ export async function runDaemon () {
|
|
|
342
342
|
const meReqFile = path.join(dir, 'me-request.json')
|
|
343
343
|
const meFile = path.join(dir, 'me.json')
|
|
344
344
|
|
|
345
|
+
/**
|
|
346
|
+
* EL PERFIL PARA `dotrino-vault me` Y LA TUI, desde el cajón `@me` (donde escribe el aparato,
|
|
347
|
+
* dato a dato). Leía el `profileGet` del almacén viejo, al que ya no escribe nadie: un perfil
|
|
348
|
+
* nuevo salía SIEMPRE vacío aunque el aparato lo hubiera mandado (dueño, 2026-09-24).
|
|
349
|
+
*
|
|
350
|
+
* `conPrivados`: con el perfil CERRADO no se dicen ni los nombres de los datos privados —el
|
|
351
|
+
* candado esconde nombres igual que con las variables—. Lo público sí: la bóveda ya se lo
|
|
352
|
+
* da a cualquiera por el proxio (`profilePublic`), así que no revela nada.
|
|
353
|
+
*/
|
|
354
|
+
function perfilParaMostrar (vault, { conPrivados }) {
|
|
355
|
+
const v = vault.profileView()
|
|
356
|
+
const lista = (s) => { if (typeof s !== 'string') return undefined; try { const a = JSON.parse(s); return Array.isArray(a) ? a : undefined } catch (_) { return undefined } }
|
|
357
|
+
const { avatar, links, fields, ...rest } = v.public
|
|
358
|
+
if (!Object.keys(v.public).length && !(conPrivados && v.private.length)) return null
|
|
359
|
+
return {
|
|
360
|
+
...rest, avatar: avatarInfo(avatar), links: lista(links), fields: lista(fields),
|
|
361
|
+
...(conPrivados ? { privateKeys: v.private } : {}), updatedAt: v.updatedAt
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
345
365
|
/** Resumen de la foto de perfil: qué es y cuánto pesa, nunca los bytes. */
|
|
346
366
|
function avatarInfo (avatar) {
|
|
347
367
|
if (typeof avatar !== 'string' || !avatar) return null
|
|
@@ -366,7 +386,8 @@ export async function runDaemon () {
|
|
|
366
386
|
const v = mgr.get(id)
|
|
367
387
|
if (!v?.rekeySecrets) return
|
|
368
388
|
const r = await v.rekeySecrets(vieja, nueva)
|
|
369
|
-
|
|
389
|
+
// Se re-sella UNA copia (la de recuperación, que abre el llavero), no cajón por cajón.
|
|
390
|
+
if (r?.rekeyed) console.log('[vault] secrets recovery copy re-sealed under the new key')
|
|
370
391
|
}
|
|
371
392
|
|
|
372
393
|
/**
|
|
@@ -808,7 +829,11 @@ export async function runDaemon () {
|
|
|
808
829
|
ipcWrite(path.join(dir, 'acta.json'), { ...closed, members: [] })
|
|
809
830
|
}
|
|
810
831
|
rm(meReqFile)
|
|
811
|
-
if (meReq)
|
|
832
|
+
if (meReq) {
|
|
833
|
+
let me = null
|
|
834
|
+
try { me = perfilParaMostrar(t.vault, { conPrivados: false }) } catch (e) { console.error('[vault] could not dump the profile:', e.message) }
|
|
835
|
+
ipcWrite(meFile, { ...closed, req: meReq.id || null, me })
|
|
836
|
+
}
|
|
812
837
|
return
|
|
813
838
|
}
|
|
814
839
|
if (dumpReq) {
|
|
@@ -852,9 +877,8 @@ export async function runDaemon () {
|
|
|
852
877
|
rm(meReqFile)
|
|
853
878
|
try {
|
|
854
879
|
const tm = resolveTarget(meReq) || { id: mgr.currentId(), vault: mgr.current() }
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
ipcWrite(meFile, { v: 1, at: Date.now(), req: meReq.id || null, profile: tm.id, me: me ? { ...rest, avatar: avatarInfo(avatar) } : null })
|
|
880
|
+
const me = perfilParaMostrar(tm.vault, { conPrivados: true })
|
|
881
|
+
ipcWrite(meFile, { v: 1, at: Date.now(), req: meReq.id || null, profile: tm.id, me })
|
|
858
882
|
} catch (e) { console.error('[vault] could not dump the profile:', e.message) }
|
|
859
883
|
}
|
|
860
884
|
} catch (e) {
|
|
@@ -1001,12 +1025,24 @@ export async function runDaemon () {
|
|
|
1001
1025
|
* Lo que arregla es lo que de verdad dolió el 2026-09-19: el replicador llevaba quince
|
|
1002
1026
|
* días atrás y las bóvedas dos versiones, y NADA lo decía en ninguna pantalla.
|
|
1003
1027
|
*
|
|
1004
|
-
*
|
|
1005
|
-
* problema del usuario, y
|
|
1028
|
+
* SI NO SE PUEDE MIRAR, SE DICE — una vez por motivo, no en cada intento. Antes se callaba
|
|
1029
|
+
* («no saber si hay versión nueva no es un problema del usuario»), y eso choca con §15:
|
|
1030
|
+
* «no se pudo mirar» NUNCA es «estás al día». El 2026-09-24 una bóveda se quedó en la
|
|
1031
|
+
* 0.130.0 con la 0.131.0 publicada y el log no decía nada: no había forma de saber si
|
|
1032
|
+
* estaba al día o si no había podido preguntar.
|
|
1006
1033
|
*/
|
|
1034
|
+
let ultimoFallo = null
|
|
1007
1035
|
const mirarRelease = async () => {
|
|
1008
|
-
|
|
1009
|
-
|
|
1036
|
+
let r
|
|
1037
|
+
try { r = await latestRelease() } catch (e) { r = { ok: false, reason: e?.message || String(e) } }
|
|
1038
|
+
if (!r.ok) {
|
|
1039
|
+
if (r.reason !== ultimoFallo) console.log(`[vault] could not check for a new version: ${r.reason} · trying again in an hour`)
|
|
1040
|
+
ultimoFallo = r.reason
|
|
1041
|
+
// Un fallo al arrancar (la red todavía no está) no puede costar un día entero.
|
|
1042
|
+
setTimeout(mirarRelease, 60 * 60_000).unref?.()
|
|
1043
|
+
return
|
|
1044
|
+
}
|
|
1045
|
+
ultimoFallo = null
|
|
1010
1046
|
ultimaPublicada = { version: r.version, checkedAt: Date.now() }
|
|
1011
1047
|
writeState()
|
|
1012
1048
|
if (!isNewer(r.version, daemonVersion)) return
|
package/src/secretsStore.js
CHANGED
|
@@ -909,6 +909,24 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
|
|
|
909
909
|
* Va firmado igual que lo demás: un dato público sin firma es un dato que cualquiera
|
|
910
910
|
* puede inventar en tu nombre, que es peor que no tenerlo.
|
|
911
911
|
*/
|
|
912
|
+
profileView () {
|
|
913
|
+
// EL PERFIL TAL COMO LO TIENE LA BÓVEDA, para enseñárselo al dueño (`dotrino-vault me`,
|
|
914
|
+
// la TUI). Sale del cajón `@me`, que es donde escribe el aparato dato a dato. Lo
|
|
915
|
+
// público va en claro; de lo privado solo el NOMBRE del dato: viaja sellado y la
|
|
916
|
+
// bóveda no lo abre para enseñarlo (§8.1), igual que una variable privada.
|
|
917
|
+
const vars = data.ns[PROFILE_NS]?.vars || {}
|
|
918
|
+
const pub = {}
|
|
919
|
+
const priv = []
|
|
920
|
+
let at = 0
|
|
921
|
+
for (const [key, e] of Object.entries(vars)) {
|
|
922
|
+
if (!e) continue
|
|
923
|
+
at = Math.max(at, e.at || 0)
|
|
924
|
+
if (e.cls === 'public' && typeof e.pubv === 'string') pub[key] = e.pubv
|
|
925
|
+
else priv.push(key)
|
|
926
|
+
}
|
|
927
|
+
return { public: pub, private: priv.sort(), updatedAt: at || null }
|
|
928
|
+
},
|
|
929
|
+
|
|
912
930
|
profilePublic () {
|
|
913
931
|
const bag = data.ns[PROFILE_NS]
|
|
914
932
|
if (!bag?.vars) return {}
|
package/src/tui/app.js
CHANGED
|
@@ -620,6 +620,11 @@ function meRows (st, t) {
|
|
|
620
620
|
if (filled.length) rows.push({ text: '', sel: false })
|
|
621
621
|
for (const [k, label] of filled) field(label, me[k], me[k + 'Visible'] === false)
|
|
622
622
|
|
|
623
|
+
// Lo PRIVADO viaja sellado y la bóveda no lo abre: se dice qué hay, no qué dice.
|
|
624
|
+
if (Array.isArray(me.privateKeys) && me.privateKeys.length) {
|
|
625
|
+
rows.push({ text: '', sel: false })
|
|
626
|
+
field(i.privateData, me.privateKeys.join(', ') + t.muted(i.privateDataHint))
|
|
627
|
+
}
|
|
623
628
|
for (const [title, list] of [[i.links, me.links], [i.otherData, me.fields]]) {
|
|
624
629
|
if (!Array.isArray(list) || !list.length) continue
|
|
625
630
|
rows.push({ text: '', sel: false })
|
package/src/tui/i18n.js
CHANGED
|
@@ -148,6 +148,8 @@ const es = {
|
|
|
148
148
|
fieldAddress: 'dirección',
|
|
149
149
|
links: 'Enlaces',
|
|
150
150
|
otherData: 'Otros datos',
|
|
151
|
+
privateData: 'Privados',
|
|
152
|
+
privateDataHint: ' (sellados: se ven en tus aparatos, no aquí)',
|
|
151
153
|
hidden: ' (oculto)',
|
|
152
154
|
no: 'no',
|
|
153
155
|
helpMe: ['←→ pestaña', 'F5 refrescar', 'Esc bóvedas', 'l English', 'q salir'],
|
|
@@ -537,6 +539,8 @@ const en = {
|
|
|
537
539
|
fieldAddress: 'address',
|
|
538
540
|
links: 'Links',
|
|
539
541
|
otherData: 'Other data',
|
|
542
|
+
privateData: 'Private',
|
|
543
|
+
privateDataHint: ' (sealed: they show on your devices, not here)',
|
|
540
544
|
hidden: ' (hidden)',
|
|
541
545
|
noName: '(no name)',
|
|
542
546
|
no: 'no',
|
package/src/vault.js
CHANGED
|
@@ -3718,6 +3718,8 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
3718
3718
|
return { ok: true, pub: found.pub, deviceId: found.deviceId }
|
|
3719
3719
|
},
|
|
3720
3720
|
listApprovals: () => approvals.list(),
|
|
3721
|
+
// El perfil desde el cajón `@me` (lo que empujan los aparatos): ver `profileView`.
|
|
3722
|
+
profileView: () => secrets.profileView(),
|
|
3721
3723
|
// LAS CONCESIONES VIVAS, para verlas y para cortarlas. Se enseñan en la máquina del
|
|
3722
3724
|
// dueño (TUI/CLI), donde el contexto ya es suyo y no hay nada que sellar.
|
|
3723
3725
|
listGrants: () => grants.list(),
|
package/src/vaultControl.js
CHANGED
|
@@ -500,7 +500,8 @@ export async function getMe (profile) {
|
|
|
500
500
|
const d = await waitFor(F.me, { req: id, since })
|
|
501
501
|
rm(F.me)
|
|
502
502
|
if (!d) throw coded('the daemon did not reply', 'NO_REPLY')
|
|
503
|
-
|
|
503
|
+
// Con el perfil cerrado llega igual lo público (ver `perfilParaMostrar` en daemon.js).
|
|
504
|
+
if (d.locked && !d.me) assertOpen(d)
|
|
504
505
|
return d.me || null
|
|
505
506
|
}
|
|
506
507
|
|