@dotrino/vault 0.93.0 → 0.93.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/src/core/vault.js CHANGED
@@ -226,7 +226,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
226
226
  *
227
227
  * SE MIRA EL PEDIDO, NUNCA EL CANAL. Sería más cómodo comprobar «vino por el socket local»,
228
228
  * y está prohibido: el día que un handler decide algo por dónde entró el mensaje, el atajo
229
- * local deja de ser un atajo y pasa a ser una puerta de atrás (`test/mostrador-local.test.mjs`).
229
+ * local deja de ser un atajo y pasa a ser una puerta de atrás (`test/local-desk.test.mjs`).
230
230
  * El `host` es un dato del pedido como cualquier otro, y mentirlo no consigue nada: lo que
231
231
  * se gana es quedar marcado como no comprobado, que es exactamente lo que se quería evitar.
232
232
  *
@@ -241,19 +241,19 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
241
241
  * llamarse como quiera. La frontera es el usuario, no el proceso: esto existe para que
242
242
  * quien aprueba decida mirando lo que pasa, y para que quede rastro.
243
243
  */
244
- function contextoDelPedido (declarado) {
245
- const dicho = sanitizeContext(declarado)
246
- if (!dicho) return null
244
+ function requestContext (declared) {
245
+ const said = sanitizeContext(declared)
246
+ if (!said) return null
247
247
  // Otra máquina: aquí no hay ningún `/proc` que hable de ese proceso.
248
- if (!dicho.host || dicho.host !== platform.hostname?.()) return { ...dicho, verified: 'declared' }
249
- const medido = sanitizeContext(platform.readProcContext?.(dicho.pid) ?? null)
248
+ if (!said.host || said.host !== platform.hostname?.()) return { ...said, verified: 'declared' }
249
+ const measured = sanitizeContext(platform.readProcContext?.(said.pid) ?? null)
250
250
  // El proceso ya no está, o no se puede mirar: no se sabe, y no saber se dice como no
251
251
  // saber. Inventar un «comprobado» aquí sería el repliegue de siempre.
252
- if (!medido) return { ...dicho, verified: 'declared' }
253
- if (commandFingerprint(medido) !== commandFingerprint(dicho)) {
254
- throw new Error(`ctx — it says it is running "${commandLine(dicho, 60)}" but pid ${dicho.pid} is "${commandLine(medido, 60)}"`)
252
+ if (!measured) return { ...said, verified: 'declared' }
253
+ if (commandFingerprint(measured) !== commandFingerprint(said)) {
254
+ throw new Error(`ctx — it says it is running "${commandLine(said, 60)}" but pid ${said.pid} is "${commandLine(measured, 60)}"`)
255
255
  }
256
- return { ...medido, user: dicho.user, host: dicho.host, verified: 'proc' }
256
+ return { ...measured, user: said.user, host: said.host, verified: 'proc' }
257
257
  }
258
258
  // LA BÓVEDA DE CONTRASEÑAS: entradas y su llave, en el dir del perfil y cifradas en
259
259
  // reposo como todo lo demás. QUIÉN PUEDE PEDIR LO DICE EL ACTA (capacidad
@@ -319,7 +319,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
319
319
  * (dueño, 2026-10-16: «realmente deberían ser uno solo»); `kind` se queda por el formato. Un miembro sin llave de cifrado no puede recibir nada: se
320
320
  * queda fuera y se ve en `incompleteMembers`.
321
321
  */
322
- const noEsLaBoveda = (pub) => pub !== master && pub !== commKey.pub()
322
+ const notTheVault = (pub) => pub !== master && pub !== commKey.pub()
323
323
  /**
324
324
  * ¿PUEDE ESCRIBIR UNA CONTRASEÑA? Un aparato del acta con `passwords`… y la MAESTRA de esta
325
325
  * bóveda, que es quien reescribe al convertir (`sealPasswords`) y al importar un respaldo.
@@ -329,7 +329,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
329
329
  */
330
330
  const canWritePasswords = (pub) => !!pub && (pub === master || (!!actaCache && Acta.memberCan(actaCache, pub, 'passwords')))
331
331
  const passwordRecipients = (_kind = 'main') => (actaCache?.members || [])
332
- .filter((m) => m.encPub && noEsLaBoveda(m.pub) && Acta.memberCan(actaCache, m.pub, 'passwords'))
332
+ .filter((m) => m.encPub && notTheVault(m.pub) && Acta.memberCan(actaCache, m.pub, 'passwords'))
333
333
  .map((m) => ({ pub: m.pub, encPub: m.encPub }))
334
334
 
335
335
  /**
@@ -355,13 +355,13 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
355
355
  * y solo para convertir lo que quedó escrito con ella; en cuanto la conversión termina,
356
356
  * `soltarLlaveVieja` la borra del archivo.
357
357
  */
358
- async function llaveViejaDeContrasenas () {
358
+ async function oldPasswordsKey () {
359
359
  const d = readPasswordsFile()
360
360
  if (!d?.cek) return null
361
361
  const raw = b64ToBytes(d.cek)
362
362
  return crypto.subtle.importKey('raw', raw, { name: 'AES-GCM' }, true, ['encrypt', 'decrypt'])
363
363
  }
364
- const soltarLlaveVieja = () => {
364
+ const dropOldKey = () => {
365
365
  const d = readPasswordsFile()
366
366
  if (!d?.cek) return
367
367
  delete d.cek
@@ -493,7 +493,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
493
493
  // `announce`: el canal de ESTA cuenta, para que una máquina prestada —que no tiene
494
494
  // ninguna llave y solo sabe la dirección `nombre@AB12-CD34-EF56`— pueda encontrar a quien
495
495
  // la atiende. Lo construye la pieza compartida, porque quien lo busca es la extensión.
496
- const { client, identify: reidentificar, isIdentified } = await createTransport({
496
+ const { client, identify: reidentify, isIdentified } = await createTransport({
497
497
  identity, url: proxyUrl, commKey, log, announce: vaultChannel(fp), makeClient: platform.makeClient || null, wakeUrl: platform.wakeUrl || null
498
498
  })
499
499
 
@@ -525,7 +525,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
525
525
  * del certificado y del acta, igual por los dos caminos. Alcanzar el socket no autoriza
526
526
  * nada — es la misma puerta, con el camino más corto.
527
527
  */
528
- const canales = new Map()
528
+ const channels = new Map()
529
529
 
530
530
  /**
531
531
  * QUÉ SOMOS, Y QUÉ CORRE CADA APARATO (CONVENCIONES §14, `@dotrino/compat`).
@@ -544,8 +544,8 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
544
544
  const MI_VERSION = compatDeclare({
545
545
  product: platform.product || 'vaultd', version: VERSION, protocol: VAULT_PROTOCOL, speaks: VAULT_SPEAKS
546
546
  })
547
- let ROTAS = []
548
- try { ROTAS = brokenOf(loadManifest()) } catch (e) { log('[vault] no compatibility registry: ' + e.message) }
547
+ let BROKEN = []
548
+ try { BROKEN = brokenOf(loadManifest()) } catch (e) { log('[vault] no compatibility registry: ' + e.message) }
549
549
 
550
550
  /**
551
551
  * Lo que dice correr cada aparato, por llave. Se apunta de lo que viene FIRMADO
@@ -553,12 +553,12 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
553
553
  * escribir no vale para enseñarla como si fuera un dato.
554
554
  */
555
555
  const versiones = new Map()
556
- const apuntarVersion = (pub, v) => {
556
+ const recordVersion = (pub, v) => {
557
557
  if (!pub || !v) return
558
- const dictamen = compatCheck({ mine: MI_VERSION, theirs: v, broken: ROTAS })
559
- versiones.set(pub, { ...v, at: Date.now(), compatible: dictamen.compatible, reason: dictamen.reason })
560
- if (!dictamen.compatible) log(`[vault] ${v.product} ${v.version}: ${dictamen.reason}`)
561
- return dictamen
558
+ const verdict = compatCheck({ mine: MI_VERSION, theirs: v, broken: BROKEN })
559
+ versiones.set(pub, { ...v, at: Date.now(), compatible: verdict.compatible, reason: verdict.reason })
560
+ if (!verdict.compatible) log(`[vault] ${v.product} ${v.version}: ${verdict.reason}`)
561
+ return verdict
562
562
  }
563
563
  /** El dictamen de un aparato, para enseñarlo o para pegárselo a un error. */
564
564
  const versionDe = (pub) => versiones.get(pub) || null
@@ -599,19 +599,19 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
599
599
  * sella. Se vacía DESPUÉS y solo lo absorbido: mientras se sella puede entrar una nueva,
600
600
  * y tirarla sería perder una renuncia sin aplicar.
601
601
  */
602
- async function absorberSubacta (motivo = 'unlock') {
603
- const pendientes = subacta.drain()
604
- if (!pendientes.length) return 0
605
- const hechas = []
606
- for (const e of pendientes) {
607
- try { await identity.absorbRenounce(e); hechas.push(e) }
608
- catch (err) { log(`[vault] ${motivo}: could not absorb a pending renounce: ${err.message}`) }
602
+ async function absorbSubacta (reason = 'unlock') {
603
+ const pendingOnes = subacta.drain()
604
+ if (!pendingOnes.length) return 0
605
+ const finished = []
606
+ for (const e of pendingOnes) {
607
+ try { await identity.absorbRenounce(e); finished.push(e) }
608
+ catch (err) { log(`[vault] ${reason}: could not absorb a pending renounce: ${err.message}`) }
609
609
  }
610
- if (hechas.length) {
611
- subacta.clear(hechas)
612
- log(`[vault] ${motivo}: ${hechas.length} pending renounce(s) absorbed into the record`)
610
+ if (finished.length) {
611
+ subacta.clear(finished)
612
+ log(`[vault] ${reason}: ${finished.length} pending renounce(s) absorbed into the record`)
613
613
  }
614
- return hechas.length
614
+ return finished.length
615
615
  }
616
616
 
617
617
  /**
@@ -626,7 +626,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
626
626
  */
627
627
  const verifyChain = async (args) => {
628
628
  const chk = await verifyChainRaw(args)
629
- if (chk.ok && args?.data?.v) apuntarVersion(chk.device, args.data.v)
629
+ if (chk.ok && args?.data?.v) recordVersion(chk.device, args.data.v)
630
630
  // DÓNDE CORRE, por el mismo motivo: se apunta de lo firmado. Un fallo al guardarlo no
631
631
  // puede tumbar el pedido —es un dato para la lista—, pero se dice.
632
632
  if (chk.ok && args?.data?.machine) {
@@ -642,9 +642,35 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
642
642
  }
643
643
 
644
644
 
645
+ /**
646
+ * LOS ERRORES DE UNA PETICIÓN DE SECRETOS VAN FIRMADOS, con la llave de sellado (la misma que
647
+ * firma lo que se sirve) y atados a esa petición (`re` = huella de su firma). Lo que va a la
648
+ * cuenta lo reciben todas sus bóvedas, y la que no puede servir (el teléfono, sin llave de
649
+ * sellado) contestaba con un error que el servicio tomaba por la respuesta: se quedaba sin
650
+ * secretos aunque esta bóveda sí se los daba (2026-10-18, accounts). Ahora el servicio solo
651
+ * obedece el error que verifica contra el acta; uno sin firma no corta nada.
652
+ *
653
+ * `errorRe`: a quién se le está contestando una petición de secretos, y con qué huella.
654
+ */
655
+ const errorRe = new Map()
656
+ async function signError (obj, re) {
657
+ const signed = { op: 'secrets.error', error: String(obj?.error || ''), re, ts: Date.now() }
658
+ const seal = await signSeal(signed)
659
+ // Sin llave de sellado no hay firma que poner: el error sale tal cual, y quien pidió no
660
+ // le hará caso (es justo el caso de la bóveda que no puede servir).
661
+ if (!seal) return { ...obj, re, signed: null }
662
+ const acta = (await identity.profileActa?.().catch(() => null))?.acta || null
663
+ return { ...obj, re, signed, seal, acta }
664
+ }
665
+
645
666
  const reply = (to, obj) => {
667
+ if (obj?.type === MSG.ERROR && !('signed' in obj) && errorRe.has(to)) {
668
+ const re = errorRe.get(to)
669
+ signError(obj, re).then((o) => reply(to, o), (e) => { log('[vault] could not sign an error:', e.message); reply(to, { ...obj, signed: null }) })
670
+ return
671
+ }
646
672
  try {
647
- const enviar = canales.get(to) || ((o) => client.send(to, o))
673
+ const send = channels.get(to) || ((o) => client.send(to, o))
648
674
  // ANUNCIAMOS EN CADA RESPUESTA: el otro lado decide con esto, y decide él porque es
649
675
  // el que puede tener la lista al día (§14). Va en el envoltorio y no dentro de un
650
676
  // cuerpo firmado a propósito — es un dato para enseñar y para explicar un fallo, no
@@ -654,9 +680,9 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
654
680
  if (bytes > MAX_REPLY_BYTES) {
655
681
  log(`[vault] the reply to ${obj?.type} does not fit (${bytes} bytes > ${MAX_REPLY_BYTES}): sending an error instead of killing the connection`)
656
682
  audit('reply-too-big', { type: obj?.type || null, bytes })
657
- return enviar({ type: MSG.ERROR, error: `reply too big (${bytes} bytes): ask for less at a time` })
683
+ return send({ type: MSG.ERROR, error: `reply too big (${bytes} bytes): ask for less at a time` })
658
684
  }
659
- enviar(obj)
685
+ send(obj)
660
686
  } catch (e) { log('[vault] could not reply:', e.message) }
661
687
  }
662
688
 
@@ -859,7 +885,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
859
885
  * mostrador deniega. Es lo contrario de un repliegue — no hay con qué decidir, así que no
860
886
  * se decide que sí.
861
887
  */
862
- async function contextoActa () {
888
+ async function actaContext () {
863
889
  const acta = (await identity.profileActa?.().catch(() => null))?.acta || null
864
890
  if (!acta) return { actaSeq: null, sealers: null }
865
891
  return { actaSeq: acta.seq, sealers: Acta.sealersOf(acta) }
@@ -887,7 +913,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
887
913
  if (!isFresh(p.data)) { audit('rejected', { what: 'sign', reason: 'stale' }); return staleReply(from) }
888
914
  const chk = await verifyChain({
889
915
  data: p.data, signature: p.signature, cert: p.cert,
890
- expectedScope: SCOPE.SIGN, ...(await contextoActa()), revoked: await revocationSet()
916
+ expectedScope: SCOPE.SIGN, ...(await actaContext()), revoked: await revocationSet()
891
917
  })
892
918
  if (!chk.ok) return denyChain(from, chk, p, 'sign')
893
919
 
@@ -923,7 +949,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
923
949
  if (!isFresh(p.data)) return staleReply(from)
924
950
  const chk = await verifyChain({
925
951
  data: p.data, signature: p.signature, cert: p.cert,
926
- expectedScope: SCOPE.READ, ...(await contextoActa()), revoked: await revocationSet()
952
+ expectedScope: SCOPE.READ, ...(await actaContext()), revoked: await revocationSet()
927
953
  })
928
954
  if (!chk.ok) return denyChain(from, chk, p, 'get')
929
955
  if (!await actaAllows(from, chk, SCOPE.READ, 'get')) return
@@ -982,22 +1008,22 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
982
1008
  },
983
1009
 
984
1010
  async open ({ nonce, enc, by }) {
985
- const guardada = aperturas.get('pending')
1011
+ const saved = aperturas.get('pending')
986
1012
  aperturas.delete('pending')
987
- if (!guardada || guardada.exp <= Date.now()) {
1013
+ if (!saved || saved.exp <= Date.now()) {
988
1014
  return { ok: false, error: 'unlock: ask for a key first, and use it within a minute' }
989
1015
  }
990
1016
  let payload = null
991
- try { payload = await openSealed({ privateKey: guardada.privateKey, enc }) } catch (_) { payload = null }
1017
+ try { payload = await openSealed({ privateKey: saved.privateKey, enc }) } catch (_) { payload = null }
992
1018
  // EL NONCE VA DENTRO DEL SOBRE. Si fuera solo por fuera, un sobre capturado se
993
1019
  // reenvía con un nonce nuevo y el candado del nonce no sirve de nada.
994
1020
  if (!payload || payload.nonce !== nonce || typeof payload.key !== 'string') {
995
1021
  audit('rejected', { what: 'unlock', by, reason: 'envelope' })
996
1022
  return { ok: false, error: 'unlock: the envelope does not check out' }
997
1023
  }
998
- const derivada = b64ToBytes(payload.key)
1024
+ const derived = b64ToBytes(payload.key)
999
1025
  try {
1000
- const r = await openWithSecondary(derivada)
1026
+ const r = await openWithSecondary(derived)
1001
1027
  audit('unlock', { by })
1002
1028
  log(`[vault] opened from the admin by ${by || '????-????'}`)
1003
1029
  return { ok: true, result: { ok: true, locked: false } }
@@ -1007,7 +1033,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1007
1033
  // que ya no responde y no sabe por qué.
1008
1034
  return { ok: false, code: e.code || null, error: e.message, tries: e.tries ?? null, waitSec: e.waitSec ?? null }
1009
1035
  } finally {
1010
- derivada.fill(0)
1036
+ derived.fill(0)
1011
1037
  }
1012
1038
  }
1013
1039
  }
@@ -1024,7 +1050,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1024
1050
  }
1025
1051
  const revoked = await revocationSet()
1026
1052
  // `vault:sign` — hablar por ti. No `vault:store`, que es guardar cosas.
1027
- const chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, expectedScope: SCOPE.SIGN, ...(await contextoActa()), revoked })
1053
+ const chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, expectedScope: SCOPE.SIGN, ...(await actaContext()), revoked })
1028
1054
  if (!chk.ok) return denyChain(from, chk, p, 'profile')
1029
1055
  if (!await actaAllows(from, chk, SCOPE.SIGN, 'profile')) return
1030
1056
 
@@ -1055,17 +1081,17 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1055
1081
  return reply(from, { type: MSG.STORE_RESULT, method: d.method, result: b })
1056
1082
  }
1057
1083
  // profilePut
1058
- const { key, cls, sobre, value } = d.args || {}
1059
- const quien = await deviceIdOf(chk.device).catch(() => null)
1084
+ const { key, cls, sobre: envelope, value } = d.args || {}
1085
+ const who = await deviceIdOf(chk.device).catch(() => null)
1060
1086
  let r
1061
1087
  if (cls === 'public') {
1062
- r = await secrets.profilePutPublic(key, value, { by: quien })
1088
+ r = await secrets.profilePutPublic(key, value, { by: who })
1063
1089
  } else {
1064
- r = await secrets.putSealed(PROFILE_OWNER, key, sobre, { by: quien })
1090
+ r = await secrets.putSealed(PROFILE_OWNER, key, envelope, { by: who })
1065
1091
  }
1066
- audit('profile.put', { key, cls: cls || 'private', by: quien, gen: r?.gen ?? null })
1092
+ audit('profile.put', { key, cls: cls || 'private', by: who, gen: r?.gen ?? null })
1067
1093
  // Y a los demás aparatos, que si no se enteran al arrancar y vuelve la divergencia.
1068
- await notifyMembers('profile', { key, by: quien })
1094
+ await notifyMembers('profile', { key, by: who })
1069
1095
  return reply(from, { type: MSG.STORE_RESULT, method: d.method, result: r })
1070
1096
  } catch (e) {
1071
1097
  return reply(from, { type: MSG.ERROR, error: 'profile: ' + e.message })
@@ -1111,9 +1137,9 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1111
1137
  return reply(from, { type: MSG.ERROR, error: 'profile locked: unlock it on the vault machine (dotrino-vault unlock) to edit it' })
1112
1138
  }
1113
1139
  const revoked = await revocationSet()
1114
- let chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, expectedScope: SCOPE.STORE, ...(await contextoActa()), revoked })
1140
+ let chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, expectedScope: SCOPE.STORE, ...(await actaContext()), revoked })
1115
1141
  if (!chk.ok && STORE_READ_METHODS.has(d.method)) {
1116
- chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, expectedScope: SCOPE.READ, ...(await contextoActa()), revoked })
1142
+ chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, expectedScope: SCOPE.READ, ...(await actaContext()), revoked })
1117
1143
  }
1118
1144
  if (!chk.ok) return denyChain(from, chk, p, 'store')
1119
1145
  // El scope que valió es el que hay que preguntarle al acta: un método de lectura pudo
@@ -1291,7 +1317,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1291
1317
 
1292
1318
  async function handleDevices (from, p) {
1293
1319
  if (!isFresh(p.data)) return staleReply(from)
1294
- const chk = await verifyChain({ data: p.data, signature: p.signature, cert: p.cert, ...(await contextoActa()), revoked: await revocationSet() })
1320
+ const chk = await verifyChain({ data: p.data, signature: p.signature, cert: p.cert, ...(await actaContext()), revoked: await revocationSet() })
1295
1321
  if (!chk.ok) return denyChain(from, chk, p, null)
1296
1322
  // DOS RESPUESTAS, PORQUE SON DOS PREGUNTAS DISTINTAS.
1297
1323
  //
@@ -1310,9 +1336,9 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1310
1336
  // génesis), y hasta ahora ningún agente headless podía: solo el navegador la tenía. Es
1311
1337
  // corta a propósito (génesis, cambios de sellador, actual), así que no pasa por `fitChain`.
1312
1338
  const sealerChainOf = async () => { try { return (await identity.sealerChain?.()) || null } catch (e) { log('[vault] could not build the sealer chain:', e.message); return null } }
1313
- const puedeVerTodo = !!record && Acta.memberCanScope(record, chk.device, SCOPE.READ)
1339
+ const canSeeAll = !!record && Acta.memberCanScope(record, chk.device, SCOPE.READ)
1314
1340
  const { issued, revoked } = await identity.listDelegations()
1315
- if (!puedeVerTodo) {
1341
+ if (!canSeeAll) {
1316
1342
  if (!record) {
1317
1343
  audit('rejected', { what: 'devices', reason: 'no-record' })
1318
1344
  return reply(from, { type: MSG.ERROR, error: 'unauthorized: this vault has no record to decide with' })
@@ -1386,7 +1412,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1386
1412
  // revocado NO puede renovarse (ahí sí toca re-emparejar con aprobación).
1387
1413
  async function handleRenew (from, p) {
1388
1414
  if (!isFresh(p.data)) { audit('rejected', { what: 'renew', reason: 'stale' }); return staleReply(from) }
1389
- const chk = await verifyChain({ data: p.data, signature: p.signature, cert: p.cert, ...(await contextoActa()), revoked: await revocationSet() })
1415
+ const chk = await verifyChain({ data: p.data, signature: p.signature, cert: p.cert, ...(await actaContext()), revoked: await revocationSet() })
1390
1416
  if (!chk.ok) return denyChain(from, chk, p, 'renew')
1391
1417
  // Reusar el label del cert original (si sigue registrado en delegations).
1392
1418
  const { issued } = await identity.listDelegations()
@@ -1450,7 +1476,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1450
1476
  if (!isValidSecretsNs(ns)) return reply(from, { type: MSG.ERROR, error: 'enckey: invalid namespace' })
1451
1477
  const chk = await verifyChain({
1452
1478
  data: p.data, signature: p.signature, cert: p.cert,
1453
- expectedScope: secretsScope(ns), ...(await contextoActa()), revoked: await revocationSet()
1479
+ expectedScope: secretsScope(ns), ...(await actaContext()), revoked: await revocationSet()
1454
1480
  })
1455
1481
  if (!chk.ok) return denyChain(from, chk, p, 'enckey')
1456
1482
  // MISMA FRONTERA QUE SERVIR EL CAJÓN, y por la misma razón: esto no solo apunta una
@@ -1477,7 +1503,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1477
1503
  // Y su cajón PROPIO también: un aparato con variables suyas se quedaba sin poder
1478
1504
  // abrirlas, que es el mismo agujero por otra puerta.
1479
1505
  for (const owner of [`ns:${ns}`, `dev:${chk.device}`]) {
1480
- await spreadKey(owner, (esPublica) => recipientsOf(owner, { public: esPublica }))
1506
+ await spreadKey(owner, (isPublic) => recipientsOf(owner, { public: isPublic }))
1481
1507
  .catch((e) => log(`[vault] ${owner}: could not hand it the key: ${e.message}`))
1482
1508
  }
1483
1509
  // Con la llave de sellado, como todo lo que sirve la bóveda: la maestra no firma con la
@@ -1518,10 +1544,10 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1518
1544
  */
1519
1545
  function drawerDigest (ns, devicePub) {
1520
1546
  const b = secrets.bundleFor(ns, devicePub)
1521
- const filas = Object.entries(b.entries || {})
1547
+ const rows = Object.entries(b.entries || {})
1522
1548
  .map(([k, e]) => [k, b.legacy ? sha256Hex(String(e.v)) : (e.gen ?? null)])
1523
1549
  .sort((a, c) => (a[0] < c[0] ? -1 : 1))
1524
- return sha256Hex(JSON.stringify(filas))
1550
+ return sha256Hex(JSON.stringify(rows))
1525
1551
  }
1526
1552
 
1527
1553
  /**
@@ -1540,7 +1566,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1540
1566
  if (!isValidSecretsNs(ns)) return reply(from, { type: MSG.ERROR, error: 'digest: invalid namespace' })
1541
1567
  const chk = await verifyChain({
1542
1568
  data: p.data, signature: p.signature, cert: p.cert,
1543
- expectedScope: secretsScope(ns), ...(await contextoActa()), revoked: await revocationSet()
1569
+ expectedScope: secretsScope(ns), ...(await actaContext()), revoked: await revocationSet()
1544
1570
  })
1545
1571
  if (!chk.ok) return denyChain(from, chk, p, 'digest')
1546
1572
  const record = (await identity.profileActa?.().catch(() => null))?.acta
@@ -1573,7 +1599,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1573
1599
  async function handleUpdateAsk (from, p) {
1574
1600
  const chk = await verifyChain({
1575
1601
  data: p.data, signature: p.signature, cert: p.cert,
1576
- ...(await contextoActa()), revoked: await revocationSet()
1602
+ ...(await actaContext()), revoked: await revocationSet()
1577
1603
  })
1578
1604
  if (!chk.ok) return denyChain(from, chk, p, 'update-ask')
1579
1605
  const record = (await identity.profileActa?.().catch(() => null))?.acta
@@ -1607,10 +1633,10 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1607
1633
  if (decided) return say(from, { ok: decided.ok, asked: true })
1608
1634
  // YA HAY UN PEDIDO VIVO por esto mismo (el agente se reinició y vuelve a preguntar): no se
1609
1635
  // timbra otra vez; la respuesta irá a quien pregunta ahora.
1610
- const vivo = updateAsks.get(key)
1611
- if (vivo && waiters.has(vivo.id)) {
1612
- vivo.dest = from
1613
- return say(from, { pending: true, id: vivo.id, exp: vivo.exp, asked: true })
1636
+ const alive = updateAsks.get(key)
1637
+ if (alive && waiters.has(alive.id)) {
1638
+ alive.dest = from
1639
+ return say(from, { pending: true, id: alive.id, exp: alive.exp, asked: true })
1614
1640
  }
1615
1641
  const ask = { id: null, exp: null, dest: from }
1616
1642
  let pend
@@ -1650,7 +1676,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1650
1676
  const ev = op === 'update-needs-root' ? 'update-needs-root' : 'updated'
1651
1677
  const chk = await verifyChain({
1652
1678
  data: p.data, signature: p.signature, cert: p.cert,
1653
- ...(await contextoActa()), revoked: await revocationSet()
1679
+ ...(await actaContext()), revoked: await revocationSet()
1654
1680
  })
1655
1681
  if (!chk.ok) return denyChain(from, chk, p, op)
1656
1682
  const record = (await identity.profileActa?.().catch(() => null))?.acta
@@ -1680,7 +1706,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1680
1706
  if (!isValidSecretsNs(ns)) return reply(from, { type: MSG.ERROR, error: `${op}: invalid namespace` })
1681
1707
  const chk = await verifyChain({
1682
1708
  data: p.data, signature: p.signature, cert: p.cert,
1683
- expectedScope: secretsScope(ns), ...(await contextoActa()), revoked: await revocationSet()
1709
+ expectedScope: secretsScope(ns), ...(await actaContext()), revoked: await revocationSet()
1684
1710
  })
1685
1711
  if (!chk.ok) return denyChain(from, chk, p, op)
1686
1712
  const record = (await identity.profileActa?.().catch(() => null))?.acta
@@ -1748,7 +1774,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1748
1774
  if (typeof p.data?.ek !== 'string') return reply(from, { type: MSG.ERROR, error: 'secrets: missing ek (requester ephemeral key)' })
1749
1775
  const chk = await verifyChain({
1750
1776
  data: p.data, signature: p.signature, cert: p.cert,
1751
- expectedScope: secretsScope(ns), ...(await contextoActa()), revoked: await revocationSet()
1777
+ expectedScope: secretsScope(ns), ...(await actaContext()), revoked: await revocationSet()
1752
1778
  })
1753
1779
  if (!chk.ok) return denyChain(from, chk, p, 'secrets')
1754
1780
  // FRONTERA DEL CN (acta): además del scope del cert, el acta tiene que decir que este
@@ -1788,7 +1814,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1788
1814
  const label = (record?.members || []).find((m) => m.pub === chk.device)?.label || ''
1789
1815
  // QUÉ COMANDO, DESDE QUÉ CARPETA — medido contra `/proc` si se puede.
1790
1816
  let ctx = null
1791
- try { ctx = contextoDelPedido(p.data?.ctx) } catch (e) {
1817
+ try { ctx = requestContext(p.data?.ctx) } catch (e) {
1792
1818
  audit('rejected', { what: 'secrets', ns, reason: 'ctx-mismatch' })
1793
1819
  log(`[vault] ${ns}: refused ${deviceId || '????-????'} — ${e.message}`)
1794
1820
  return reply(from, { type: MSG.ERROR, code: 'ctx-mismatch', error: 'unauthorized: ' + e.message })
@@ -1796,12 +1822,12 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1796
1822
  // ¿YA DIJISTE QUE SÍ A ESTE COMANDO? Entonces no se vuelve a timbrar: la concesión
1797
1823
  // dura una hora DESDE EL ÚLTIMO USO, así que un servicio que sigue pidiendo la
1798
1824
  // mantiene viva. Cambia el comando o la carpeta y esto ya no encaja: otro pedido.
1799
- const vigente = grants.allows({ ns, device: chk.device, ctx })
1800
- if (vigente) {
1825
+ const current = grants.allows({ ns, device: chk.device, ctx })
1826
+ if (current) {
1801
1827
  try {
1802
1828
  const res = await resultFor(ns, chk.device, p.data.ek, record, { publicOnly: false })
1803
- audit('secrets', { device: deviceId, ns, grant: vigente.id })
1804
- log(`[vault] ${ns}: ${deviceId || '????-????'} served without ringing (approved command, ${vigente.uses} use(s))`)
1829
+ audit('secrets', { device: deviceId, ns, grant: current.id })
1830
+ log(`[vault] ${ns}: ${deviceId || '????-????'} served without ringing (approved command, ${current.uses} use(s))`)
1805
1831
  return reply(from, { type: MSG.SECRETS_RESULT, ...res })
1806
1832
  } catch (e) {
1807
1833
  return reply(from, { type: MSG.ERROR, error: 'secrets: invalid ek' })
@@ -1813,7 +1839,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1813
1839
  // volver POR DONDE VINO. Si la pregunta entró por el mostrador local, mandarla por el
1814
1840
  // proxio la deja en el vacío: quien preguntó está escuchando en el socket, no ahí.
1815
1841
  // Lo cazó el E2E de aprobación, que se quedó esperando cinco minutos.
1816
- const pend = approvals.request({ ns, device: chk.device, deviceId, label, ek: p.data.ek, ctx, from })
1842
+ const pend = approvals.request({ ns, device: chk.device, deviceId, label, ek: p.data.ek, ctx, from, re: errorRe.get(from) || null })
1817
1843
  audit('secrets.pending', { device: deviceId, ns, id: pend.id })
1818
1844
  log(`[vault] ${ns}: ${deviceId || '????-????'} is waiting for approval (${pend.id})` +
1819
1845
  (ctx ? ` — ${commandLine(ctx, 80)} [${ctx.cwd || '?'}]` : ' — it does not say what it is running'))
@@ -1857,11 +1883,11 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1857
1883
  // aparecer una y otra vez. Falta la envoltura, y eso solo se arregla ABRIENDO la
1858
1884
  // bóveda; decirlo aquí es lo que convierte un bucle mudo en algo que se puede arreglar.
1859
1885
  if (!b.legacy && !publicOnly) {
1860
- const faltan = [...secrets.missingFor(`ns:${ns}`, devicePub), ...secrets.missingFor(`dev:${devicePub}`, devicePub)]
1861
- if (faltan.length) {
1862
- const quien = await deviceIdOf(devicePub).catch(() => '????-????')
1863
- audit('secrets.no-wrapping', { device: quien, ns, vars: faltan })
1864
- log(`[vault] ${ns}: ${quien} has no wrapping for ${faltan.join(', ')} — open the vault (dotrino-vault unlock) so it hands it the key`)
1886
+ const missingOnes = [...secrets.missingFor(`ns:${ns}`, devicePub), ...secrets.missingFor(`dev:${devicePub}`, devicePub)]
1887
+ if (missingOnes.length) {
1888
+ const who = await deviceIdOf(devicePub).catch(() => '????-????')
1889
+ audit('secrets.no-wrapping', { device: who, ns, vars: missingOnes })
1890
+ log(`[vault] ${ns}: ${who} has no wrapping for ${missingOnes.join(', ')} — open the vault (dotrino-vault unlock) so it hands it the key`)
1865
1891
  }
1866
1892
  }
1867
1893
  const enc = await seal({ ek, payload })
@@ -1903,7 +1929,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1903
1929
  const op = p.data?.op
1904
1930
  const chk = await verifyChain({
1905
1931
  data: p.data, signature: p.signature, cert: p.cert,
1906
- ...(await contextoActa()), revoked: await revocationSet()
1932
+ ...(await actaContext()), revoked: await revocationSet()
1907
1933
  })
1908
1934
  if (!chk.ok) return denyChain(from, chk, p, 'approval')
1909
1935
  const record = (await identity.profileActa?.().catch(() => null))?.acta
@@ -1923,11 +1949,11 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1923
1949
  }
1924
1950
  // `notices`: lo que no es un pedido y quien aprueba tiene que saber (la bóveda se
1925
1951
  // actualizó). El timbre de una app nativa llega vacío, así que lo lee de aquí.
1926
- if (op === 'approvals') return answer({ op: 'approvals', items: await conContextoSellado(approvals.list(), chk.device, record), notices: notices.list() })
1952
+ if (op === 'approvals') return answer({ op: 'approvals', items: await withSealedContext(approvals.list(), chk.device, record), notices: notices.list() })
1927
1953
  // LO QUE YA ESTÁ APROBADO SE VE Y SE CORTA DESDE DONDE SE APROBÓ. Una concesión que se
1928
1954
  // renueva con cada uso puede durar indefinidamente: si no se puede mirar ni quitar desde
1929
1955
  // el mismo sitio donde se dio, es un permiso invisible, y eso no lo damos por bueno.
1930
- if (op === 'grants') return answer({ op: 'grants', items: await conContextoSellado(grants.list(), chk.device, record), ttlMs: GRANT_TTL_MS })
1956
+ if (op === 'grants') return answer({ op: 'grants', items: await withSealedContext(grants.list(), chk.device, record), ttlMs: GRANT_TTL_MS })
1931
1957
  // LOS BLOQUEADOS SE VEN desde donde se bloquean. Desbloquear no: eso es de la bóveda
1932
1958
  // (`dotrino-vault unblock <ID>`) o del administrador, que es lo que pidió el dueño.
1933
1959
  if (op === 'blocked') return answer({ op: 'blocked', items: blocked.list() })
@@ -1991,9 +2017,10 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
1991
2017
  audit('secrets.denied', { device: pend.deviceId, ns: pend.ns, id, by })
1992
2018
  log(`[vault] ${pend.ns}: request of ${pend.deviceId} DENIED by ${by}`)
1993
2019
  try {
1994
- const err = { type: MSG.ERROR, error: `unauthorized: denied — the "${pend.ns}" request was denied from ${by}` }
1995
- const porLocal = pend.from && canales.get(pend.from)
1996
- if (porLocal) porLocal(err)
2020
+ const plain = { type: MSG.ERROR, error: `unauthorized: denied — the "${pend.ns}" request was denied from ${by}` }
2021
+ const err = pend.re ? await signError(plain, pend.re) : plain
2022
+ const byLocal = pend.from && channels.get(pend.from)
2023
+ if (byLocal) byLocal(err)
1997
2024
  else client.sendByPubkey(pend.device, err)
1998
2025
  } catch (e) { log(`[vault] ${pend.ns}: could not tell ${pend.deviceId} it was denied: ${e.message}`) }
1999
2026
  return answer({ op: 'deny.result', id, ok: true })
@@ -2020,8 +2047,8 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
2020
2047
  // Por el canal por el que preguntó. Si fue el proxio, `sendByPubkey`: así lo recoge al
2021
2048
  // volver aunque ahora esté apagado. Si fue el socket local, por el socket.
2022
2049
  try {
2023
- const porLocal = pend.from && canales.get(pend.from)
2024
- if (porLocal) porLocal({ type: MSG.SECRETS_RESULT, ...res })
2050
+ const byLocal = pend.from && channels.get(pend.from)
2051
+ if (byLocal) byLocal({ type: MSG.SECRETS_RESULT, ...res })
2025
2052
  else client.sendByPubkey(pend.device, { type: MSG.SECRETS_RESULT, ...res })
2026
2053
  } catch (e) { log(`[vault] ${pend.ns}: approved, but the reply did not reach ${pend.deviceId}: ${e.message}`) }
2027
2054
  return answer({ op: 'approve.result', id, ok: true })
@@ -2042,26 +2069,26 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
2042
2069
  * en vez de mandárselo en claro «para que al menos lo vea»: eso sería cambiar la promesa
2043
2070
  * por comodidad, y encima sin avisar a nadie.
2044
2071
  */
2045
- async function conContextoSellado (items, aprobador, record) {
2046
- const encPub = (record?.members || []).find((m) => m.pub === aprobador)?.encPub || null
2047
- const salida = []
2072
+ async function withSealedContext (items, approver, record) {
2073
+ const encPub = (record?.members || []).find((m) => m.pub === approver)?.encPub || null
2074
+ const output = []
2048
2075
  for (const it of items) {
2049
- const { ctx, ...resto } = it
2050
- if (!ctx) { salida.push({ ...resto, ctx: null }); continue }
2051
- if (!encPub) { salida.push({ ...resto, ctx: null, ctxSealed: false, ctxReason: 'no-enc-key' }); continue }
2076
+ const { ctx, ...rest } = it
2077
+ if (!ctx) { output.push({ ...rest, ctx: null }); continue }
2078
+ if (!encPub) { output.push({ ...rest, ctx: null, ctxSealed: false, ctxReason: 'no-enc-key' }); continue }
2052
2079
  try {
2053
2080
  const cek = await makeContentKey()
2054
2081
  const envelope = await encryptWithCek({ cek, gen: 0, plaintext: JSON.stringify(ctx) })
2055
2082
  const wrap = await wrapForMember({ cek, memberEncPub: encPub })
2056
- salida.push({ ...resto, ctx: null, ctxSealed: true, ctxWrap: wrap, ctxEnvelope: envelope })
2083
+ output.push({ ...rest, ctx: null, ctxSealed: true, ctxWrap: wrap, ctxEnvelope: envelope })
2057
2084
  } catch (e) {
2058
2085
  // NO SE TRAGA: sin esto, un fallo al sellar se vería en el teléfono igual que un
2059
2086
  // pedido que no dice nada, y nadie sabría que hay algo que arreglar.
2060
- log(`[vault] could not seal the command of ${it.id} for ${aprobador.slice(0, 24)}…: ${e.message}`)
2061
- salida.push({ ...resto, ctx: null, ctxSealed: false, ctxReason: 'seal-failed' })
2087
+ log(`[vault] could not seal the command of ${it.id} for ${approver.slice(0, 24)}…: ${e.message}`)
2088
+ output.push({ ...rest, ctx: null, ctxSealed: false, ctxReason: 'seal-failed' })
2062
2089
  }
2063
2090
  }
2064
- return salida
2091
+ return output
2065
2092
  }
2066
2093
 
2067
2094
  /**
@@ -2153,7 +2180,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
2153
2180
  */
2154
2181
  async function handleIncident (from, p) {
2155
2182
  if (!isFresh(p.data)) return staleReply(from)
2156
- const chk = await verifyChain({ data: p.data, signature: p.signature, cert: p.cert, ...(await contextoActa()), revoked: await revocationSet() })
2183
+ const chk = await verifyChain({ data: p.data, signature: p.signature, cert: p.cert, ...(await actaContext()), revoked: await revocationSet() })
2157
2184
  if (!chk.ok) return denyChain(from, chk, p, 'incident')
2158
2185
  const record = await refreshActa()
2159
2186
  if (!record || !Acta.memberCan(record, chk.device, 'sign')) {
@@ -2208,7 +2235,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
2208
2235
  if (identity.me?.publickey && typeof platform.openLocalDesk === 'function') {
2209
2236
  platform.openLocalDesk({
2210
2237
  name: (await pubkeyId(identity.me.publickey)).slice(0, 16),
2211
- onMessage, channels: canales, maxBytes: MAX_REPLY_BYTES, log
2238
+ onMessage, channels: channels, maxBytes: MAX_REPLY_BYTES, log
2212
2239
  })
2213
2240
  }
2214
2241
 
@@ -2224,7 +2251,13 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
2224
2251
  if (payload.type === MSG.CHECK) return await handleCheck(from, payload)
2225
2252
  if (payload.type === MSG.DEVICES) return await handleDevices(from, payload)
2226
2253
  if (payload.type === MSG.RENEW) return await handleRenew(from, payload)
2227
- if (payload.type === MSG.SECRETS) return await handleSecrets(from, payload)
2254
+ if (payload.type === MSG.SECRETS) {
2255
+ const re = sha256Hex(String(payload.signature || ''))
2256
+ errorRe.set(from, re)
2257
+ try { return await handleSecrets(from, payload) } catch (e) {
2258
+ return reply(from, { type: MSG.ERROR, error: e.message })
2259
+ } finally { if (errorRe.get(from) === re) errorRe.delete(from) }
2260
+ }
2228
2261
  if (payload.type === MSG.REWRAP_OK) return await handleRewrapOk(payload)
2229
2262
  if (payload.type === MSG.ADMIN) return await handleAdmin(from, payload)
2230
2263
  if (payload.type === MSG.RENOUNCE) return await handleRenounce(from, payload)
@@ -2257,8 +2290,8 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
2257
2290
  // («sin-acta-previa»), y este es el único mostrador donde una llega de un desconocido.
2258
2291
  // Anunciar no es cómo se entra en una cuenta —para eso está el enrolamiento—, así que
2259
2292
  // sin acta propia no se adopta: se dice y se para.
2260
- const propia = (await identity.profileActa?.().catch(() => null))?.acta || null
2261
- if (!propia) return log('[vault] a record was announced but this vault has none of its own to compare it with: ignored')
2293
+ const own = (await identity.profileActa?.().catch(() => null))?.acta || null
2294
+ if (!own) return log('[vault] a record was announced but this vault has none of its own to compare it with: ignored')
2262
2295
  try {
2263
2296
  const r = await identity.adoptActa?.(acta)
2264
2297
  if (r?.adopted) log(`[vault] adopted record #${acta.seq} announced by another vault (${r.reason})`)
@@ -2321,13 +2354,13 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
2321
2354
  */
2322
2355
  async function approvePasswordRequest ({ pubkey, op, payload }) {
2323
2356
  let resolve
2324
- const espera = new Promise((r) => { resolve = r })
2357
+ const wait = new Promise((r) => { resolve = r })
2325
2358
  const fields = Array.isArray(payload?.keys) ? payload.keys.slice(0, 12).map((k) => String(k).slice(0, 40)) : null
2326
2359
  await awaitApproval({
2327
2360
  device: pubkey, ns: 'passwords', kind: 'passwords', what: 'passwords', op,
2328
2361
  ctx: { op: String(op || '').replace(/^pm2\./, ''), fields }, onAnswer: resolve
2329
2362
  })
2330
- return espera
2363
+ return wait
2331
2364
  }
2332
2365
 
2333
2366
  const startPasswordDesk = async () => {
@@ -2529,14 +2562,14 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
2529
2562
  //
2530
2563
  // El acta es la lista de quién es del perfil: esa es la lista correcta. `issued` se
2531
2564
  // suma porque cubre a quien tiene cert y todavía no aparece ahí.
2532
- const miembros = (await identity.profileMembers?.().catch(() => null))?.members || []
2565
+ const members = (await identity.profileMembers?.().catch(() => null))?.members || []
2533
2566
  const yo = identity.me?.publickey || null
2534
2567
  // UNO POR LLAVE, no uno por delegación: renovar emite una delegación nueva para la
2535
2568
  // MISMA sub-clave, así que un aparato que lleve tiempo enrolado aparece varias veces
2536
2569
  // y recibía el mismo aviso repetido —una vez por renovación acumulada—. Mismo
2537
2570
  // cuidado que en `notifyNsChange`.
2538
2571
  const seen = new Set()
2539
- const avisar = (pub) => {
2572
+ const notify = (pub) => {
2540
2573
  if (!pub || pub === yo || seen.has(pub)) return
2541
2574
  seen.add(pub)
2542
2575
  // El fallo NO se traga. Que un aviso no salga es exactamente lo que deja a dos
@@ -2549,8 +2582,8 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
2549
2582
  try { client.sendByPubkey(pub, { type: MSG.ADMIN_EVENT, body, acta }, { quiet: true, app: 'vault' }) }
2550
2583
  catch (e) { log(`[vault] could not notify ${pub.slice(0, 24)}… of "${ev}": ${e.message}`) }
2551
2584
  }
2552
- for (const m of miembros) avisar(m.pub)
2553
- for (const d of issued || []) avisar(d.sub)
2585
+ for (const m of members) notify(m.pub)
2586
+ for (const d of issued || []) notify(d.sub)
2554
2587
  log(`[vault] notified ${seen.size} member(s) of "${ev}" (record #${acta?.seq ?? '?'})`)
2555
2588
  } catch (e) { log('[vault] could not notify members of the change:', e.message) }
2556
2589
  // Y a los replicadores, que no quieren un aviso sino el CONTENIDO: sin esto se
@@ -2578,11 +2611,11 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
2578
2611
  // LA LLAVE VA DENTRO DEL MENSAJE FIRMADO, no se saca de `from`: `from` es el
2579
2612
  // identificador de la conexión en el proxio, no la llave del miembro. Compararlo con
2580
2613
  // el acta rechazaba a todo el mundo, y en el caso contrario habría sido peor.
2581
- const quien = body.publickey
2582
- if (typeof quien !== 'string' || !Acta.memberCan(record, quien, 'replica')) {
2614
+ const who = body.publickey
2615
+ if (typeof who !== 'string' || !Acta.memberCan(record, who, 'replica')) {
2583
2616
  return log('[vault] a replica ack arrived from someone the record does not allow to serve: ignored')
2584
2617
  }
2585
- if (!(await verifyDeviceSig({ publickey: quien, data: body, signature: p.signature }))) {
2618
+ if (!(await verifyDeviceSig({ publickey: who, data: body, signature: p.signature }))) {
2586
2619
  return log('[vault] a replica ack does not check out against its own key: ignored')
2587
2620
  }
2588
2621
  if (typeof body.seq === 'number' && body.seq >= record.seq) return
@@ -2603,7 +2636,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
2603
2636
  * peligro —el cliente rechaza un acta con `seq` menor del que ya vio— sino menos
2604
2637
  * disponibilidad, que es exactamente lo que un replicador arriesga.
2605
2638
  */
2606
- async function pushToReplicas (motivo = 'change') {
2639
+ async function pushToReplicas (reason = 'change') {
2607
2640
  try {
2608
2641
  const record = (await identity.profileActa?.().catch(() => null))?.acta || null
2609
2642
  if (!record) return
@@ -2625,7 +2658,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
2625
2658
  try { client.sendByPubkey(r.pub, { type: MSG.REPLICA_PUSH, body, seal }); n++ }
2626
2659
  catch (e) { log(`[vault] could not push to a replica: ${e.message}`) }
2627
2660
  }
2628
- log(`[vault] pushed record #${record.seq} and ${Object.keys(bundles).length} bundle(s) to ${n} replica(s) on ${motivo}`)
2661
+ log(`[vault] pushed record #${record.seq} and ${Object.keys(bundles).length} bundle(s) to ${n} replica(s) on ${reason}`)
2629
2662
  } catch (e) { log('[vault] could not push to the replicas: ' + e.message) }
2630
2663
  }
2631
2664
 
@@ -2903,7 +2936,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
2903
2936
  verify: async ({ data, signature, cert }) => {
2904
2937
  const chk = await verifyChain({
2905
2938
  data, signature, cert,
2906
- expectedScope: SCOPE.ADMIN, ...(await contextoActa()), revoked: await revocationSet()
2939
+ expectedScope: SCOPE.ADMIN, ...(await actaContext()), revoked: await revocationSet()
2907
2940
  })
2908
2941
  // Quitarse a UNO MISMO desde la consola remota entra por aquí: la segunda petición
2909
2942
  // que mande el aparato ya llega con el certificado retirado. Que se entere con el
@@ -2952,8 +2985,8 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
2952
2985
  audit('rejected', { what: 'renounce', reason: 'no-record' })
2953
2986
  return reply(from, { type: MSG.ERROR, error: 'renounce: this vault has no record to apply it to' })
2954
2987
  }
2955
- const tiene = new Set(Acta.effectiveCaps(actual, record.member) || [])
2956
- if (!(record.caps || []).some((c) => tiene.has(c))) {
2988
+ const has = new Set(Acta.effectiveCaps(actual, record.member) || [])
2989
+ if (!(record.caps || []).some((c) => has.has(c))) {
2957
2990
  audit('rejected', { what: 'renounce', reason: 'nothing-to-remove' })
2958
2991
  return reply(from, { type: MSG.ERROR, error: 'renounce: that member no longer holds any of those capabilities' })
2959
2992
  }
@@ -2962,8 +2995,8 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
2962
2995
  // esté la bóveda abierta o cerrada. Antes se intentaba sellar directamente, sellar es
2963
2996
  // de la maestra, y con el perfil cerrado la renuncia se perdía con un error — en el
2964
2997
  // único momento en que de verdad hace falta.
2965
- const guardada = await subacta.add(record)
2966
- if (!guardada.ok && guardada.reason === 'not-acceptable') {
2998
+ const saved = await subacta.add(record)
2999
+ if (!saved.ok && saved.reason === 'not-acceptable') {
2967
3000
  audit('rejected', { what: 'renounce', reason: 'not-acceptable' })
2968
3001
  return reply(from, { type: MSG.ERROR, error: 'renounce: the entry is not one this vault accepts' })
2969
3002
  }
@@ -3142,13 +3175,13 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
3142
3175
 
3143
3176
  /** Sin duplicar: un cosellador puede ser además el dueño del cajón. */
3144
3177
  /** Sin repetidos por llave: un aparato puede entrar por dos vías (dueño y admin). */
3145
- const dedup = (miembros) => {
3146
- const vistos = new Set()
3147
- return miembros.filter((m) => m && !vistos.has(m.pub) && vistos.add(m.pub))
3178
+ const dedup = (members) => {
3179
+ const seenOnes = new Set()
3180
+ return members.filter((m) => m && !seenOnes.has(m.pub) && seenOnes.add(m.pub))
3148
3181
  }
3149
- const conCoselladores = (base, co) => {
3150
- const vistos = new Set(base.map((m) => m.pub))
3151
- return [...base, ...co.filter((m) => !vistos.has(m.pub))]
3182
+ const withCosealers = (base, co) => {
3183
+ const seenOnes = new Set(base.map((m) => m.pub))
3184
+ return [...base, ...co.filter((m) => !seenOnes.has(m.pub))]
3152
3185
  }
3153
3186
 
3154
3187
  async function recipientsOf (owner, { public: isPublic = false } = {}) {
@@ -3174,23 +3207,23 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
3174
3207
  // normal firma y no administra, y tiene que ver tu perfil.
3175
3208
  if (owner === PROFILE_OWNER) {
3176
3209
  const record = (await identity.profileActa?.().catch(() => null))?.acta || null
3177
- const firman = (record?.members || [])
3210
+ const signers = (record?.members || [])
3178
3211
  .filter((m) => m.encPub && Acta.memberCan(record, m.pub, 'sign'))
3179
3212
  .map((m) => ({ pub: m.pub, encPub: m.encPub }))
3180
- return conCoselladores(dedup(firman), co)
3213
+ return withCosealers(dedup(signers), co)
3181
3214
  }
3182
3215
  if (owner.startsWith('ns:')) {
3183
3216
  const owned = await nsMembers(owner.slice(3))
3184
3217
  // Sin dueño (un cajón personal, o uno cuyo servicio ya no está) entra quien administra
3185
3218
  // SIEMPRE: si no, no quedaría nadie que pudiera abrirlo sin la frase.
3186
3219
  const base = owned.length ? [...owned, ...admins] : await adminDevices()
3187
- return conCoselladores(dedup(base), co)
3220
+ return withCosealers(dedup(base), co)
3188
3221
  }
3189
3222
  const pub = owner.slice(owner.indexOf(':') + 1)
3190
3223
  const m = await memberOf(pub)
3191
3224
  // El cajón propio de un aparato de SERVICIO es tan suyo como el de su ns.
3192
- if (m?.cn) return conCoselladores(dedup([m, ...admins]), co)
3193
- return conCoselladores(dedup([...(m ? [m] : []), ...await adminDevices()]), co)
3225
+ if (m?.cn) return withCosealers(dedup([m, ...admins]), co)
3226
+ return withCosealers(dedup([...(m ? [m] : []), ...await adminDevices()]), co)
3194
3227
  }
3195
3228
 
3196
3229
  /**
@@ -3284,10 +3317,10 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
3284
3317
  for (const owner of secrets.owners()) {
3285
3318
  // Por VISIBILIDAD: a quien administra se le envuelve la pública de un cajón con dueño
3286
3319
  // y no la privada. La lista de las públicas contiene siempre a la de las privadas.
3287
- const privados = new Set((await recipientsOf(owner)).map((m) => m.pub))
3320
+ const privateOnes = new Set((await recipientsOf(owner)).map((m) => m.pub))
3288
3321
  for (const m of await recipientsOf(owner, { public: true })) {
3289
3322
  if (!m.encPub || m.pub === master) continue
3290
- const missing = secrets.missingFor(owner, m.pub, { only: (esPublica) => esPublica || privados.has(m.pub) })
3323
+ const missing = secrets.missingFor(owner, m.pub, { only: (isPublic) => isPublic || privateOnes.has(m.pub) })
3291
3324
  if (!missing.length) continue
3292
3325
  const row = out.get(m.pub) || { pub: m.pub, cn: m.cn || null, owners: {} }
3293
3326
  row.owners[owner] = missing
@@ -3556,8 +3589,8 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
3556
3589
  if (m.cn && !allowOverwrite && secrets.has?.(owner, key)) {
3557
3590
  throw Object.assign(new Error(`var.set: "${m.cn}" cannot overwrite "${key}" — without an approval a service only fills in what is missing; replacing is for who administers`), { code: 'exists' })
3558
3591
  }
3559
- const cuerpo = authorBody(owner, key, sealed.e, a.ts)
3560
- if (!(await verifyDeviceSig({ publickey: a.pub, data: cuerpo, signature: a.sig }))) {
3592
+ const body = authorBody(owner, key, sealed.e, a.ts)
3593
+ if (!(await verifyDeviceSig({ publickey: a.pub, data: body, signature: a.sig }))) {
3561
3594
  throw new Error('var.set: the author signature does not verify over this envelope')
3562
3595
  }
3563
3596
  }
@@ -3576,19 +3609,19 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
3576
3609
  if (!wraps || typeof wraps !== 'object') throw new Error('var.set: the envelope carries no wraps')
3577
3610
  const { recoveryPub, members } = await secrets.recipientsFor(owner, { public: isPublic })
3578
3611
  if (!recoveryPub) throw new Error('var.set: this vault has no recovery key yet')
3579
- const deben = new Set(members.map((m) => m.pub))
3580
- const traidos = new Set(Object.keys(wraps).filter((k) => k !== RECOVERY_WRAP))
3581
- const faltan = [...deben].filter((p) => !traidos.has(p))
3582
- const sobran = [...traidos].filter((p) => !deben.has(p))
3583
- if (faltan.length) {
3584
- throw new Error(`var.set: the envelope leaves ${faltan.length} member(s) of "${owner}" out — they could not read it (ask the vault for the recipients with \`var.recipients\`)`)
3612
+ const mustHave = new Set(members.map((m) => m.pub))
3613
+ const fetched = new Set(Object.keys(wraps).filter((k) => k !== RECOVERY_WRAP))
3614
+ const missingOnes = [...mustHave].filter((p) => !fetched.has(p))
3615
+ const extra = [...fetched].filter((p) => !mustHave.has(p))
3616
+ if (missingOnes.length) {
3617
+ throw new Error(`var.set: the envelope leaves ${missingOnes.length} member(s) of "${owner}" out — they could not read it (ask the vault for the recipients with \`var.recipients\`)`)
3585
3618
  }
3586
- if (sobran.length) {
3587
- throw new Error(`var.set: the envelope wraps for ${sobran.length} key(s) the record does not name for "${owner}"`)
3619
+ if (extra.length) {
3620
+ throw new Error(`var.set: the envelope wraps for ${extra.length} key(s) the record does not name for "${owner}"`)
3588
3621
  }
3589
3622
  }
3590
3623
 
3591
- async function refreshWraps (motivo) {
3624
+ async function refreshWraps (reason) {
3592
3625
  // HAY CANDADO SOLO SI HAY LAS DOS COSAS: contraseña Y alguien que sepa abrirla.
3593
3626
  //
3594
3627
  // `startVault` sin gestor de perfiles trae `hasPassword = () => true` y `openKey =
@@ -3597,18 +3630,18 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
3597
3630
  // con la llave de la máquina, y el repaso del llavero se saltaba ENTERO: el servicio se
3598
3631
  // quedaba sin su envoltura y contestábamos un paquete que no podía abrir. Es el mismo
3599
3632
  // fallo que se está arreglando, colado por la puerta de al lado.
3600
- const conCandado = typeof openKey === 'function' && (() => { try { return !!hasPassword() } catch (_) { return true } })()
3601
- const k = conCandado ? openKey() : null
3602
- if (conCandado && !k) {
3603
- log(`[vault] ${motivo}: the record changed but the profile key is not here — the record cannot be sealed while locked, so this should not happen; the keyring was NOT rebuilt`)
3633
+ const withLock = typeof openKey === 'function' && (() => { try { return !!hasPassword() } catch (_) { return true } })()
3634
+ const k = withLock ? openKey() : null
3635
+ if (withLock && !k) {
3636
+ log(`[vault] ${reason}: the record changed but the profile key is not here — the record cannot be sealed while locked, so this should not happen; the keyring was NOT rebuilt`)
3604
3637
  return null
3605
3638
  }
3606
- const r = await resealAll(k).catch((e) => { log(`[vault] ${motivo}: could not rebuild the keyring: ${e.message}`); return null })
3607
- if (r?.wrapped) log(`[vault] ${motivo}: keyring rebuilt (${r.wrapped} wrap(s) in ${r.drawers} drawer(s))`)
3639
+ const r = await resealAll(k).catch((e) => { log(`[vault] ${reason}: could not rebuild the keyring: ${e.message}`); return null })
3640
+ if (r?.wrapped) log(`[vault] ${reason}: keyring rebuilt (${r.wrapped} wrap(s) in ${r.drawers} drawer(s))`)
3608
3641
  // LOS SOBRES CADUCOS SE DICEN. Son envolturas de quien ya no debería poder abrir ese
3609
3642
  // cajón: retirarlas en silencio esconde que alguien las tuvo de más.
3610
- if (r?.dropped) log(`[vault] ${motivo}: ${r.dropped} stale wrap(s) dropped (they belonged to members the record no longer names)`)
3611
- if (r?.failed?.length) log(`[vault] ${motivo}: ${r.failed.length} drawer(s) could NOT be resealed: ${r.failed.map((f) => f.owner).join(', ')}`)
3643
+ if (r?.dropped) log(`[vault] ${reason}: ${r.dropped} stale wrap(s) dropped (they belonged to members the record no longer names)`)
3644
+ if (r?.failed?.length) log(`[vault] ${reason}: ${r.failed.length} drawer(s) could NOT be resealed: ${r.failed.map((f) => f.owner).join(', ')}`)
3612
3645
  return r
3613
3646
  }
3614
3647
 
@@ -3632,9 +3665,9 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
3632
3665
  *
3633
3666
  * Se puede quitar cuando no queden perfiles de antes de 0.81.0 (2027-03-01).
3634
3667
  */
3635
- const MIGRACION_RECUPERACION_HASTA = Date.parse('2027-03-01T00:00:00Z')
3636
- async function migrarRecuperacionALaFrase (adminKey) {
3637
- if (!adminKey || Date.now() > MIGRACION_RECUPERACION_HASTA) return false
3668
+ const RECOVERY_MIGRATION_UNTIL = Date.parse('2027-03-01T00:00:00Z')
3669
+ async function migrateRecoveryToPhrase (adminKey) {
3670
+ if (!adminKey || Date.now() > RECOVERY_MIGRATION_UNTIL) return false
3638
3671
  try {
3639
3672
  // Si la frase ya la abre, no hay nada que migrar.
3640
3673
  await secrets.recoveryOpensWith(adminKey)
@@ -3646,9 +3679,9 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
3646
3679
  // pasar `null` probaba la misma llave dos veces y la migración no migraba nada. Lo
3647
3680
  // dijo el propio diagnóstico: «no abre ni con la contraseña ni con la de la máquina»,
3648
3681
  // cuando la de la máquina no se había llegado a probar.
3649
- const maquina = disk.machineKey()
3650
- await secrets.recoveryOpensWith(maquina) // si esta tampoco, no hay migración que hacer
3651
- const r = await secrets.rekeyRecovery(maquina, adminKey)
3682
+ const machine = disk.machineKey()
3683
+ await secrets.recoveryOpensWith(machine) // si esta tampoco, no hay migración que hacer
3684
+ const r = await secrets.rekeyRecovery(machine, adminKey)
3652
3685
  if (!r?.rekeyed) return false
3653
3686
  log('[vault] the recovery copy was sealed with this machine\'s key: re-sealed under the profile password')
3654
3687
  audit('secret.recovery-rekeyed', {})
@@ -3686,12 +3719,12 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
3686
3719
  const r = await convertPasswords({
3687
3720
  store: passwordsStore,
3688
3721
  sealed: passwords.sealed,
3689
- cek: await llaveViejaDeContrasenas(),
3722
+ cek: await oldPasswordsKey(),
3690
3723
  recipients,
3691
3724
  // LA MAESTRA FIRMA LO QUE ESCRIBE, como cualquier aparato que guarda una entrada.
3692
3725
  // Firmar no es leer: lo que se le envuelve a esta bóveda sigue siendo nada.
3693
3726
  author: { publickey: master, sign: (body) => identity.signData(body) },
3694
- dropOldKey: soltarLlaveVieja,
3727
+ dropOldKey: dropOldKey,
3695
3728
  log
3696
3729
  })
3697
3730
  out.converted = r?.entries || 0
@@ -3720,7 +3753,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
3720
3753
  async function resealAll (adminKey = null) {
3721
3754
  // ANTES de reenvolver nada: si la copia de recuperación se quedó bajo la llave de la
3722
3755
  // máquina, se pasa a la frase. Sin esto, todo lo de abajo falla con «wrong password».
3723
- if (adminKey) await migrarRecuperacionALaFrase(adminKey)
3756
+ if (adminKey) await migrateRecoveryToPhrase(adminKey)
3724
3757
  // Y LA LLAVE DE RECUPERACIÓN EXISTE. Nacía al escribir el primer secreto, así que una
3725
3758
  // bóveda recién creada no tenía a quién envolver — y como crearla necesita la maestra,
3726
3759
  // cerrada ya no había manera. El síntoma: escribir el perfil con la bóveda cerrada
@@ -3736,7 +3769,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
3736
3769
  // POR VISIBILIDAD, no una lista fija: una pública lleva además la envoltura de quien
3737
3770
  // administra. Con una sola lista se le daría también la de una privada con dueño, que
3738
3771
  // es justo lo que no puede pasar.
3739
- const members = (esPublica) => recipientsOf(owner, { public: esPublica })
3772
+ const members = (isPublic) => recipientsOf(owner, { public: isPublic })
3740
3773
  try {
3741
3774
  const r = await secrets.rewrap(owner, members, adminKey, { exact: true })
3742
3775
  const after = new Set(secrets.recipientsIn(owner))
@@ -3770,7 +3803,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
3770
3803
  // Los destinatarios de un cajón no son solo quien lo consume: también quien lo
3771
3804
  // administra (`recipientsOf`). Envolver solo para los primeros dejaba a la consola
3772
3805
  // sin poder destapar lo que ella misma acababa de saldar.
3773
- const members = (esPublica) => recipientsOf(owner, { public: esPublica })
3806
+ const members = (isPublic) => recipientsOf(owner, { public: isPublic })
3774
3807
  try { out[owner] = await spreadKey(owner, members, adminKey) } catch (e) {
3775
3808
  // Sin frase, el último recurso es pedírselo a quien sí puede: el propio servicio.
3776
3809
  let delegated = 0
@@ -3804,7 +3837,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
3804
3837
  // (dueño, 2026-09-01: «al envolver hay que envolver siempre para todos los selladores,
3805
3838
  // para que siempre tengan la info fresca»). Dos listas para la misma pregunta acaban
3806
3839
  // siempre así: una se actualiza y la otra no.
3807
- try { return await spreadKey(owner, (esPublica) => recipientsOf(owner, { public: esPublica }), null) } catch (_) { return null }
3840
+ try { return await spreadKey(owner, (isPublic) => recipientsOf(owner, { public: isPublic }), null) } catch (_) { return null }
3808
3841
  }
3809
3842
  async function deleteSecret (ns, key) { const ok = await secrets.delete(ns, key); if (ok) { audit('secret.rm', { ns, key }); scheduleNotice(ns) } return ok }
3810
3843
 
@@ -4137,10 +4170,10 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
4137
4170
  try {
4138
4171
  const members = (await identity.profileMembers()).members
4139
4172
  for (const { pub, caps } of changes) {
4140
- const quedaron = new Set(members.find((m) => m.pub === pub)?.caps || [])
4141
- const perdidos = caps.filter((c) => !quedaron.has(c))
4142
- if (perdidos.length) {
4143
- log(`[vault] WARNING these permissions were DROPPED: ${perdidos.join(', ')} — this build's @dotrino/identity does not know them (record unchanged for those)`)
4173
+ const remaining = new Set(members.find((m) => m.pub === pub)?.caps || [])
4174
+ const lost = caps.filter((c) => !remaining.has(c))
4175
+ if (lost.length) {
4176
+ log(`[vault] WARNING these permissions were DROPPED: ${lost.join(', ')} — this build's @dotrino/identity does not know them (record unchanged for those)`)
4144
4177
  }
4145
4178
  }
4146
4179
  } catch (_) { /* comprobar es un extra: si falla, no rompe el cambio */ }
@@ -4260,20 +4293,20 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
4260
4293
  // la bóveda seguía muda en el proxio con su llave ya admitida en el acta. Al dueño le
4261
4294
  // pasó el 2026-09-05: hizo lo que el log pedía y la consola siguió sin contestar.
4262
4295
  if (r?.locked === false) {
4263
- try { if (await ensureCommKeyInActa()) await reidentificar?.() } catch (_) {}
4296
+ try { if (await ensureCommKeyInActa()) await reidentify?.() } catch (_) {}
4264
4297
  }
4265
4298
  // Y ABSORBER LO PENDIENTE. La subacta ya lo estaba honrando con el perfil cerrado —de
4266
4299
  // eso vive—, pero mientras siga ahí solo lo sabe esta máquina: al sellarlo, entra en
4267
4300
  // el acta y lo respeta toda la cuenta. Si falla no se arrastra: abrir el perfil no
4268
4301
  // puede depender de esto, y lo pendiente sigue surtiendo efecto igual.
4269
- if (r?.locked === false) { try { await absorberSubacta('unlock') } catch (_) {} }
4302
+ if (r?.locked === false) { try { await absorbSubacta('unlock') } catch (_) {} }
4270
4303
  // Y LOS APARATOS MUERTOS FUERA DEL ACTA, en una sola (dueño, 2026-09-22). Es el mismo
4271
4304
  // momento: la maestra está en memoria y cambiar el acta es uno de sus dos trabajos.
4272
4305
  // Si falla no se arrastra —abrir no depende de esto—, pero se dice.
4273
4306
  if (r?.locked === false) {
4274
4307
  try {
4275
4308
  const antes = await refreshActa()
4276
- const aprobaban = (antes?.members || []).filter((m) => Acta.memberCan(antes, m.pub, 'approve')).map((m) => m.pub)
4309
+ const approvedBefore = (antes?.members || []).filter((m) => Acta.memberCan(antes, m.pub, 'approve')).map((m) => m.pub)
4277
4310
  const { removed = [], seq } = await identity.pruneExpiredDevices?.() || {}
4278
4311
  for (const d of removed) {
4279
4312
  const id = await deviceIdOf(d.pub).catch(() => null)
@@ -4284,14 +4317,14 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
4284
4317
  // el papel muerto no podía firmar ninguna aprobación, estuviera o no en el acta—,
4285
4318
  // pero desde ese momento todo pedido de un aparato sin `unattended` se rechaza, y
4286
4319
  // el 2026-09-22 eso pasó en silencio: el teléfono se fue y nadie lo supo en dos días.
4287
- const despues = await refreshActa()
4288
- const quedan = (despues?.members || []).filter((m) => Acta.memberCan(despues, m.pub, 'approve')).map((m) => m.pub)
4289
- const idosPub = lastApproverGone(aprobaban, quedan, removed.map((d) => d.pub))
4290
- const idos = removed.filter((d) => idosPub.includes(d.pub))
4291
- if (idos.length) {
4292
- const quienes = (await Promise.all(idos.map(async (d) => `${await deviceIdOf(d.pub).catch(() => '????-????')}${d.label ? ` (${d.label})` : ''}`))).join(', ')
4293
- audit('no-approver', { removed: idos.length, seq })
4294
- log(`[vault] WARNING: nobody in the record can approve any more — ${quienes} was the last. From now on every device gets its keys WITHOUT approval, until you enrol one and grant it \`dotrino-vault caps <ID> +aprueba\``)
4320
+ const after = await refreshActa()
4321
+ const left = (after?.members || []).filter((m) => Acta.memberCan(after, m.pub, 'approve')).map((m) => m.pub)
4322
+ const gonePubs = lastApproverGone(approvedBefore, left, removed.map((d) => d.pub))
4323
+ const gone = removed.filter((d) => gonePubs.includes(d.pub))
4324
+ if (gone.length) {
4325
+ const whom = (await Promise.all(gone.map(async (d) => `${await deviceIdOf(d.pub).catch(() => '????-????')}${d.label ? ` (${d.label})` : ''}`))).join(', ')
4326
+ audit('no-approver', { removed: gone.length, seq })
4327
+ log(`[vault] WARNING: nobody in the record can approve any more — ${whom} was the last. From now on every device gets its keys WITHOUT approval, until you enrol one and grant it \`dotrino-vault caps <ID> +aprueba\``)
4295
4328
  }
4296
4329
  } catch (e) { log(`[vault] unlock: could not remove expired devices: ${e.message}`) }
4297
4330
  }
@@ -4335,7 +4368,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
4335
4368
  const answer = new Promise((resolve) => { settle = resolve })
4336
4369
  // RETIRAR: otro perfil ya contestó por la máquina, o ya no hace falta. El pedido sale
4337
4370
  // de la lista de quien aprueba en vez de quedarse un día pidiendo algo ya decidido.
4338
- const retirar = () => {
4371
+ const retire = () => {
4339
4372
  if (!pendId || !waiters.delete(pendId)) return
4340
4373
  const pend = approvals.take(pendId)
4341
4374
  audit('update.withdrawn', { device: pend?.deviceId || null, ns: 'vault', id: pendId })
@@ -4345,9 +4378,9 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
4345
4378
  awaitApproval({
4346
4379
  device: yo, ns: 'vault', kind: 'update', what: 'update',
4347
4380
  ctx: { product: '@dotrino/vaultd', version, from: VERSION }, ttlMs: UPDATE_TTL_MS, onAnswer: settle
4348
- }).then((pend) => { pendId = pend.id; if (withdrawn) retirar() })
4381
+ }).then((pend) => { pendId = pend.id; if (withdrawn) retire() })
4349
4382
  .catch((e) => { log(`[vault] update: could not ask for approval: ${e.message}`); settle(false) })
4350
- return { answer, withdraw: () => { withdrawn = true; retirar() } }
4383
+ return { answer, withdraw: () => { withdrawn = true; retire() } }
4351
4384
  },
4352
4385
  announceUpdate,
4353
4386
  approvers: async () => {
@@ -4409,7 +4442,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
4409
4442
  // dueño (TUI/CLI), donde el contexto ya es suyo y no hay nada que sellar.
4410
4443
  listGrants: () => grants.list(),
4411
4444
  revokeGrant: (id) => grants.revoke(id),
4412
- revokeGrants: (filtro) => grants.revokeAll(filtro || {}),
4445
+ revokeGrants: (filter) => grants.revokeAll(filter || {}),
4413
4446
  // Aprobar desde el PC avisa igual que aprobar a distancia: el resto de tus
4414
4447
  // dispositivos se entera de que entró alguien, venga de donde venga.
4415
4448
  approveDevice: async (code, adminKey) => {
@@ -4426,7 +4459,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
4426
4459
  // envolver hay que envolver siempre para todos los selladores, para que siempre
4427
4460
  // tengan la info fresca»). Repartiendo solo a los del ns, un cajón tocado por esta
4428
4461
  // puerta dejaba a los selladores con la envoltura vieja.
4429
- await spreadKey(`ns:${m.cn}`, (esPublica) => recipientsOf(`ns:${m.cn}`, { public: esPublica }), adminKey).catch(async (e) => {
4462
+ await spreadKey(`ns:${m.cn}`, (isPublic) => recipientsOf(`ns:${m.cn}`, { public: isPublic }), adminKey).catch(async (e) => {
4430
4463
  log('[vault] could not hand the key to the new service:', e.message)
4431
4464
  // Sin la frase la bóveda no puede envolvérsela… pero un HERMANO suyo sí: otro
4432
4465
  // servicio del mismo cajón ya tiene la llave abierta (§8.11). Si contesta, el
@@ -4557,10 +4590,10 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
4557
4590
  */
4558
4591
  localChannel (onReply) {
4559
4592
  const id = 'local:' + Math.random().toString(36).slice(2)
4560
- canales.set(id, (obj) => { try { onReply(obj) } catch (_) {} })
4593
+ channels.set(id, (obj) => { try { onReply(obj) } catch (_) {} })
4561
4594
  return {
4562
4595
  send: (payload) => { onMessage(id, payload).catch((e) => reply(id, { type: MSG.ERROR, error: e.message })) },
4563
- close: () => { canales.delete(id) }
4596
+ close: () => { channels.delete(id) }
4564
4597
  }
4565
4598
  },
4566
4599
  revokeDevice: async (target) => {