@dotrino/vaultd 0.96.0 → 0.97.0

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.96.0",
3
+ "version": "0.97.0",
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": {
package/src/daemon.js CHANGED
@@ -333,10 +333,13 @@ export async function runDaemon () {
333
333
  // cajón envuelto para exactamente quien dice el acta — creando lo que falta,
334
334
  // reemplazando lo que alguien metiera mal y quitando lo que sobre.
335
335
  const r = await mgr.get(id)?.resealAll?.(ak)
336
- if (r?.wrapped) console.log('[vault] keyring rebuilt on unlock: %d wrap(s) in %d drawer(s)%s',
337
- r.wrapped, r.drawers, r.dropped ? `, ${r.dropped} stale one(s) dropped` : '')
336
+ if (r?.wrapped) console.log('[vault] keyring rebuilt on unlock: %d wrap(s) in %d drawer(s)%s%s',
337
+ r.wrapped, r.drawers, r.dropped ? `, ${r.dropped} stale one(s) dropped` : '',
338
+ r.sealed ? `, ${r.sealed} value(s) left in the clear now sealed` : '')
338
339
  if (r?.dropped) note = ` · llavero al día (${r.dropped} envoltura(s) de más retirada(s))`
339
340
  else if (r?.wrapped) note = ' · llavero al día'
341
+ // Se SUMA, no pisa: es lo que el dueño necesita ver de esta apertura en concreto.
342
+ if (r?.sealed) note += ` · ${r.sealed} valor(es) que estaban sin cifrar ya están sellados`
340
343
  // ABIERTA PERO SIN LLAVERO NO ES «ABIERTA». Si un cajón no se pudo reenvolver, sus
341
344
  // aparatos no van a poder leer nada y hay que decirlo AQUÍ: contestar
342
345
  // «desbloqueado» a secas dejaba la bóveda pareciendo sana, y el fallo aparecía
@@ -834,11 +834,44 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
834
834
  if (!bag[k]) return { wrapped: 0, sinLlave: [] }
835
835
  const priv = await openRecovery(adminKey)
836
836
  let wrapped = 0
837
+ let sealed = 0
837
838
  const sinLlave = new Set()
839
+ const vars = varsOf(bag, k)
840
+ const paraVisibilidad = async (esPublica) => (typeof members === 'function' ? members(esPublica) : members)
841
+
842
+ // UN VALOR QUE SE QUEDÓ EN CLARO SE SELLA AQUÍ, que es el único sitio donde se puede.
843
+ //
844
+ // Una pública escrita antes del 2026-09-02 se guardó legible y SIN sobre, así que no
845
+ // tiene generación ni entrada en el llavero. Y el llavero es lo único que este
846
+ // método reenvuelve: por muchas veces que se abra la bóveda, a esa variable no se le
847
+ // crea una envoltura nunca. Sus aparatos la reclaman para siempre («no key yet») y la
848
+ // consola invita a abrir la bóveda, que es justo lo que no lo arregla. Peor: desde
849
+ // 2026-09-02 el agente rechaza el cajón ENTERO por ella, así que una variable de
850
+ // prueba deja sin leer el token que está al lado.
851
+ //
852
+ // Con la bóveda abierta el valor está delante, así que se cifra como si se acabara
853
+ // de escribir. No es un repliegue ni una migración a medias: es la conversión que
854
+ // faltaba, y ocurre en el único momento en que hay con qué hacerla.
855
+ for (const [key, e] of Object.entries(vars)) {
856
+ if (typeof e.v !== 'string' || e.e) continue
857
+ const cek = await sealer.newKey()
858
+ const gen = (topGen(bag, k)?.gen || 0) + 1
859
+ const r = await sealer.wrapFor(cek, await paraVisibilidad(!!e.pub))
860
+ r.wraps[RECOVERY] = await sealer.wrapForKey(cek, data.recovery.pub)
861
+ const sobre = await sealer.encrypt(cek, e.v, gen)
862
+ const seal = signer ? await signer({ owner, key, gen, iv: sobre.iv, ct: sobre.ct }) : null
863
+ // `pub` y `at` se conservan: no es un valor nuevo, es el mismo mejor guardado.
864
+ vars[key] = { pub: !!e.pub, owner, gen, e: sobre, seal, at: e.at || Date.now(), by: e.by || null }
865
+ bag[k].keyring = [...(bag[k].keyring || []), { gen, createdAt: Date.now(), wraps: r.wraps }]
866
+ for (const x of r.sinLlave) sinLlave.add(x)
867
+ sealed++
868
+ }
869
+
838
870
  // De qué variable es cada generación: es lo que dice si esa CEK es de una pública.
871
+ // Se calcula DESPUÉS de sellar, para que las generaciones recién creadas entren.
839
872
  const visibilidad = new Map()
840
- for (const e of Object.values(varsOf(bag, k))) if (e.gen != null) visibilidad.set(e.gen, !!e.pub)
841
- const paraGen = async (gen) => (typeof members === 'function' ? members(visibilidad.get(gen) === true) : members)
873
+ for (const e of Object.values(vars)) if (e.gen != null) visibilidad.set(e.gen, !!e.pub)
874
+ const paraGen = async (gen) => paraVisibilidad(visibilidad.get(gen) === true)
842
875
  for (const g of bag[k].keyring || []) {
843
876
  const w = g.wraps?.[RECOVERY]
844
877
  if (!w) continue
@@ -859,7 +892,7 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
859
892
  wrapped += Object.keys(r.wraps).length
860
893
  }
861
894
  save()
862
- return { wrapped, sinLlave: [...sinLlave] }
895
+ return { wrapped, sealed, sinLlave: [...sinLlave] }
863
896
  },
864
897
 
865
898
  /**
package/src/vault.js CHANGED
@@ -2291,7 +2291,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
2291
2291
  // ANTES de reenvolver nada: si la copia de recuperación se quedó bajo la llave de la
2292
2292
  // máquina, se pasa a la frase. Sin esto, todo lo de abajo falla con «wrong password».
2293
2293
  if (adminKey) await migrarRecuperacionALaFrase(adminKey)
2294
- const out = { drawers: 0, wrapped: 0, dropped: 0, sinLlave: [], failed: [] }
2294
+ const out = { drawers: 0, wrapped: 0, sealed: 0, dropped: 0, sinLlave: [], failed: [] }
2295
2295
  for (const owner of secrets.owners?.() || []) {
2296
2296
  const before = new Set(secrets.recipientsIn(owner))
2297
2297
  // POR VISIBILIDAD, no una lista fija: una pública lleva además la envoltura de quien
@@ -2303,6 +2303,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
2303
2303
  const after = new Set(secrets.recipientsIn(owner))
2304
2304
  out.drawers++
2305
2305
  out.wrapped += r?.wrapped || 0
2306
+ out.sealed += r?.sealed || 0
2306
2307
  out.dropped += [...before].filter((p) => !after.has(p)).length
2307
2308
  for (const p of r?.sinLlave || []) out.sinLlave.push(p)
2308
2309
  } catch (e) {
@@ -2310,7 +2311,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
2310
2311
  out.failed.push({ owner, error: e.message })
2311
2312
  }
2312
2313
  }
2313
- if (out.dropped) audit('secret.reseal', { drawers: out.drawers, dropped: out.dropped })
2314
+ if (out.dropped || out.sealed) audit('secret.reseal', { drawers: out.drawers, dropped: out.dropped, sealed: out.sealed })
2314
2315
  if (out.failed.length) audit('secret.reseal-failed', { drawers: out.failed.map((f) => f.owner) })
2315
2316
  return out
2316
2317
  }