@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vaultd",
3
- "version": "0.130.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.1",
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
- assertOpen(dump)
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
- if (r?.rekeyed) console.log('[vault] secrets master re-sealed (%d drawer(s))', r.drawers)
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) ipcWrite(meFile, { ...closed, req: meReq.id || null, me: null })
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 { me } = tm.vault.threads.methods.profileGet()
856
- const { avatar, ...rest } = me || {}
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
- * Si no se puede mirar, no se dice nada y ya está: no saber si hay versión nueva no es un
1005
- * problema del usuario, y llenarle el log de fallos de red tampoco ayuda.
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
- const r = await latestRelease()
1009
- if (!r.ok) return
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
@@ -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(),
@@ -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
- assertOpen(d)
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