@dotrino/vaultd 0.19.0 → 0.22.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.
@@ -42,6 +42,8 @@ export const MSG = Object.freeze({
42
42
  DEVICES: 'vault.devices', // dispositivo → vault: { data:{publickey,ts}, signature, cert }
43
43
  DEVICES_RESULT: 'vault.devices.result', // vault → dispositivo: { devices, revoked }
44
44
  RENEW: 'vault.renew', // dispositivo → vault: { data:{op,publickey,ts}, signature, cert }
45
+ RENOUNCE: 'vault.renounce', // dispositivo → vault: { record } (RENUNCIA firmada por el propio miembro)
46
+ RENOUNCE_RESULT: 'vault.renounce.result', // vault → dispositivo: { ok, seq }
45
47
  RENEWED: 'vault.renewed', // vault → dispositivo: { cert } (cert fresco, misma sub-clave/scope)
46
48
  SECRETS: 'vault.secrets', // servicio → vault: { data:{op,ns,ek,publickey,ts}, signature, cert }
47
49
  SECRETS_RESULT: 'vault.secrets.result', // vault → servicio: { body:{op,ns,enc,ts}, signature } (enc SELLADO a ek; body firmado por la maestra)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vaultd",
3
- "version": "0.19.0",
3
+ "version": "0.22.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": {
@@ -19,7 +19,7 @@
19
19
  "node": ">=20"
20
20
  },
21
21
  "dependencies": {
22
- "@dotrino/identity": "^0.39.0",
22
+ "@dotrino/identity": "^0.42.0",
23
23
  "@dotrino/proxy-client": "^0.10.0",
24
24
  "ws": "^8.18.0"
25
25
  },
package/src/ctl.js CHANGED
@@ -329,7 +329,7 @@ async function cmdMembers () {
329
329
  for (const m of acta.members) {
330
330
  const quien = m.label || m.id
331
331
  const marcas = [
332
- m.isMaster ? `${B}manda${Z}` : null,
332
+ m.isMaster ? `${B}Master${Z}` : null,
333
333
  m.isMe ? 'este dispositivo' : null,
334
334
  m.cn ? `servicio «${m.cn}»` : null
335
335
  ].filter(Boolean)
package/src/tui/i18n.js CHANGED
@@ -242,7 +242,7 @@ const es = {
242
242
  errNotApplied: 'El daemon no aplicó el cambio (revisa los logs del servicio).',
243
243
  errNotDeleted: 'El daemon no borró la variable (revisa los logs del servicio).',
244
244
  errPairFailed: 'El daemon no inició el emparejamiento.',
245
- errMasterWithMembers: 'Esta cuenta la manda esta bóveda y tiene otros dispositivos: pásale primero el mando a uno que esté conectado. Si la borras así, se quedan con su llave y sin nadie que pueda volver a firmar el acta.'
245
+ errMasterWithMembers: 'Esta bóveda es el Master de esta cuenta y hay otros dispositivos: pásale primero el Master a uno que esté conectado. Si la borras así, se quedan con su llave y sin nadie que pueda volver a firmar el acta.'
246
246
  }
247
247
 
248
248
  // ---------------------------------- inglés ----------------------------------
@@ -451,7 +451,7 @@ const en = {
451
451
  errNotApplied: 'The daemon did not apply the change (check the service logs).',
452
452
  errNotDeleted: 'The daemon did not delete the variable (check the service logs).',
453
453
  errPairFailed: 'The daemon did not start the pairing.',
454
- errMasterWithMembers: 'This vault is in charge of this account and it has other devices: hand the lead over to one that is online first. If you delete it like this, they keep their key with nobody able to sign the record again.'
454
+ errMasterWithMembers: 'This vault is the Master of this account and it has other devices: hand the Master over to one that is online first. If you delete it like this, they keep their key with nobody able to sign the record again.'
455
455
  }
456
456
 
457
457
  // --------------------------- selección y persistencia -----------------------
package/src/vault.js CHANGED
@@ -229,9 +229,16 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
229
229
  // que falta (ventana de retención, §1.3) para que compruebe el encadenamiento en vez
230
230
  // de tragarse un salto a ciegas. Si se salió de la ventana, llega vacía y toca
231
231
  // re-emparejar — que es justo lo que debe pasar.
232
- const chain = typeof p.data?.sinceSeq === 'number'
233
- ? (await identity.actaHistory({ sinceSeq: p.data.sinceSeq }).catch(() => null))?.chain || null
234
- : null
232
+ // El `.catch()` de antes NO protegía de nada: si `actaHistory` no existe, el
233
+ // TypeError es SÍNCRONO y salta antes de que haya promesa que encadenar. Pasó: el
234
+ // método vivía en el núcleo pero no en el envoltorio de Node, así que esta línea
235
+ // tiraba toda la respuesta y ningún navegador ya emparejado —que manda `sinceSeq`
236
+ // siempre— volvía a sincronizar su acta. Un try/catch de verdad, y opcional.
237
+ let chain = null
238
+ if (typeof p.data?.sinceSeq === 'number') {
239
+ try { chain = (await identity.actaHistory?.({ sinceSeq: p.data.sinceSeq }))?.chain || null }
240
+ catch (e) { log('[vault] could not build the record chain:', e.message) }
241
+ }
235
242
  // `sub` (pubkey completa) va incluida: es la DIRECCIÓN de cada dispositivo en el
236
243
  // proxy → permite a las apps AUTODESCUBRIR tus máquinas (p. ej. la terminal
237
244
  // lista tus agentes sin pegar nada). Solo la ve quien presenta un cert tuyo válido.
@@ -324,6 +331,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
324
331
  if (payload.type === MSG.RENEW) return await handleRenew(from, payload)
325
332
  if (payload.type === MSG.SECRETS) return await handleSecrets(from, payload)
326
333
  if (payload.type === MSG.ADMIN) return await handleAdmin(from, payload)
334
+ if (payload.type === MSG.RENOUNCE) return await handleRenounce(from, payload)
327
335
  } catch (e) {
328
336
  reply(from, { type: MSG.ERROR, error: e.message })
329
337
  }
@@ -449,6 +457,34 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
449
457
  }
450
458
  })
451
459
 
460
+ /**
461
+ * RENUNCIA: un miembro se quita capacidades a sí mismo y la bóveda la SELLA en el acta.
462
+ *
463
+ * NO se pide certificado, y es a propósito: el registro va firmado por el propio miembro
464
+ * y solo puede QUITAR, así que honrarlo no puede hacer daño — y exigir un cert válido
465
+ * dejaría fuera justo el caso que la justifica (un aparato que ya no es de fiar, o cuyo
466
+ * cert caducó). Sin esto la renuncia se quedaba en el dispositivo: la bóveda seguía
467
+ * teniendo escrito que podía firmar y le seguía aceptando peticiones.
468
+ */
469
+ async function handleRenounce (from, p) {
470
+ const record = p?.data?.record || p?.record
471
+ if (!record || typeof record !== 'object') return reply(from, { type: MSG.ERROR, error: 'renounce: record required' })
472
+ if (!(await Acta.verifyRenounce(record).catch(() => false))) {
473
+ audit('rejected', { what: 'renounce', reason: 'signature' })
474
+ return reply(from, { type: MSG.ERROR, error: 'renounce: the signature is not the member own' })
475
+ }
476
+ try {
477
+ const r = await identity.absorbRenounce(record)
478
+ const device = await deviceIdOf(record.member).catch(() => null)
479
+ audit('renounce', { device, caps: record.caps })
480
+ log(`[vault] ${device} renounced: ${(record.caps || []).join(', ')}`)
481
+ await notifyMembers('renounce', { deviceId: device, caps: record.caps })
482
+ reply(from, { type: MSG.RENOUNCE_RESULT, ok: true, seq: r?.seq ?? null })
483
+ } catch (e) {
484
+ reply(from, { type: MSG.ERROR, error: 'renounce: ' + e.message })
485
+ }
486
+ }
487
+
452
488
  async function handleAdmin (from, p) {
453
489
  // La frescura se comprueba aquí (es del transporte, igual que en el resto de
454
490
  // handlers); el resto de la regla vive en el módulo puro.