@dotrino/vaultd 0.75.0 → 0.76.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/lib/src/service.js +17 -4
- package/package.json +2 -2
- package/src/vault.js +36 -9
package/lib/src/service.js
CHANGED
|
@@ -421,7 +421,7 @@ export async function registerEncKey ({ dir, ns, proxyUrl, masterPubkey, device,
|
|
|
421
421
|
* Renueva el cert automáticamente si está por vencer (best-effort).
|
|
422
422
|
* @returns {Promise<Record<string,string>>} secretos KEY→valor
|
|
423
423
|
*/
|
|
424
|
-
export async function fetchSecrets ({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, timeoutMs = 30000, onPending, approvalTimeoutMs = APPROVAL_TIMEOUT_MS } = {}) {
|
|
424
|
+
export async function fetchSecrets ({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, timeoutMs = 30000, onPending, onCert, approvalTimeoutMs = APPROVAL_TIMEOUT_MS } = {}) {
|
|
425
425
|
let saved = null
|
|
426
426
|
if (dir) saved = readServiceIdentity(dir)
|
|
427
427
|
// Sin `dir`: la identidad viene entera por parámetros — es el caso de un agente
|
|
@@ -465,7 +465,15 @@ export async function fetchSecrets ({ dir, ns, proxyUrl, masterPubkey, device, c
|
|
|
465
465
|
recordar({ actaSeq: acta.seq })
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
-
|
|
468
|
+
// DOS MOTIVOS PARA PEDIR PAPEL NUEVO:
|
|
469
|
+
// · el nuestro es del MODELO VIEJO (lleva `exp` y no `seq`). Ese hay que cambiarlo sí
|
|
470
|
+
// o sí: se acepta por el repliegue de migración, pero muere en su fecha y con él el
|
|
471
|
+
// servicio. Sin esta rama la migración no terminaba nunca — el disparador comparaba
|
|
472
|
+
// `seq` contra `seq`, y un papel viejo no tiene ninguno, así que no saltaba jamás.
|
|
473
|
+
// · o el acta que hemos visto va por delante del papel: el dueño cambió permisos.
|
|
474
|
+
const legado = !!cert && typeof cert.seq !== 'number'
|
|
475
|
+
const atrasado = typeof saved?.actaSeq === 'number' && typeof cert?.seq === 'number' && saved.actaSeq > cert.seq
|
|
476
|
+
const renovar = legado || atrasado
|
|
469
477
|
|
|
470
478
|
// RENOVAR YA NO ES UNA TAREA DE CALENDARIO. El papel no vence: lo único que obliga a
|
|
471
479
|
// pedir otro es que la bóveda diga que el nuestro se quedó atrás respecto del acta, y
|
|
@@ -481,6 +489,11 @@ export async function fetchSecrets ({ dir, ns, proxyUrl, masterPubkey, device, c
|
|
|
481
489
|
if (res.type === MSG.RENEWED && res.cert?.sub === device.publickey) {
|
|
482
490
|
cert = res.cert
|
|
483
491
|
recordar({ cert })
|
|
492
|
+
// QUIEN NO PASA `dir` GUARDA ÉL. Un servicio que lleva su identidad en su propio
|
|
493
|
+
// archivo (`link.json`) y nos pasa el enlace a mano no tiene `saved`, así que
|
|
494
|
+
// `recordar` no escribe nada: sin esto renovaba en cada arranque y volvía a
|
|
495
|
+
// empezar, y su papel del modelo viejo no se cambiaba NUNCA.
|
|
496
|
+
try { onCert?.(cert) } catch (_) {}
|
|
484
497
|
}
|
|
485
498
|
} catch (_) { /* la renovación no bloquea el fetch */ }
|
|
486
499
|
}
|
|
@@ -978,11 +991,11 @@ function fingerprintOf (secrets) {
|
|
|
978
991
|
* servicio no opera hasta que esto resuelva — esa es la regla.
|
|
979
992
|
* @returns {Promise<Record<string,string>>}
|
|
980
993
|
*/
|
|
981
|
-
export async function waitForSecrets ({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, retryMs = 5000, maxRetryMs = 60000, onRetry, onPending } = {}) {
|
|
994
|
+
export async function waitForSecrets ({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, retryMs = 5000, maxRetryMs = 60000, onRetry, onPending, onCert } = {}) {
|
|
982
995
|
let delay = retryMs
|
|
983
996
|
for (;;) {
|
|
984
997
|
try {
|
|
985
|
-
return await fetchSecrets({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, onPending })
|
|
998
|
+
return await fetchSecrets({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, onPending, onCert })
|
|
986
999
|
} catch (e) {
|
|
987
1000
|
// Lo NO transitorio no se arregla reintentando: falta de enrolamiento,
|
|
988
1001
|
// cert revocado/vencido o scope equivocado exigen re-emparejar → se corta.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.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": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"node": ">=20"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@dotrino/identity": "0.74.
|
|
23
|
+
"@dotrino/identity": "0.74.1",
|
|
24
24
|
"@dotrino/passmanager": "^0.9.0",
|
|
25
25
|
"@dotrino/proxy-client": "^0.13.1",
|
|
26
26
|
"ws": "^8.18.0"
|
package/src/vault.js
CHANGED
|
@@ -615,14 +615,29 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
615
615
|
if (!isFresh(p.data)) return staleReply(from)
|
|
616
616
|
const chk = await verifyChain({ data: p.data, signature: p.signature, cert: p.cert, ...(await contextoActa()), revoked: await revocationSet() })
|
|
617
617
|
if (!chk.ok) return denyChain(from, chk, p, null)
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
|
|
618
|
+
// DOS RESPUESTAS, PORQUE SON DOS PREGUNTAS DISTINTAS.
|
|
619
|
+
//
|
|
620
|
+
// Un SERVICIO pregunta aquí para enterarse de dos cosas suyas: si le revocaron el papel
|
|
621
|
+
// y cuál es el acta vigente. Eso no es tu inventario y no puede exigir `lee` — se lo
|
|
622
|
+
// exigí, y en producción dejó ciegos a los servicios (`rejected devices/acta` en la
|
|
623
|
+
// bitácora del VPS, dos veces, antes de que lo viera).
|
|
624
|
+
//
|
|
625
|
+
// La LISTA DE APARATOS sí es el perfil —cómo se llama cada máquina y qué puede— y esa
|
|
626
|
+
// sigue pidiendo `lee`. Antes no preguntaba nada, así que un aparato al que le quitaste
|
|
627
|
+
// el permiso seguía viéndolo todo hasta que su papel caducara.
|
|
628
|
+
const record = (await identity.profileActa?.().catch(() => null))?.acta || null
|
|
629
|
+
const puedeVerTodo = !!record && Acta.memberCanScope(record, chk.device, SCOPE.READ)
|
|
622
630
|
const { issued, revoked } = await identity.listDelegations()
|
|
631
|
+
if (!puedeVerTodo) {
|
|
632
|
+
if (!record) {
|
|
633
|
+
audit('rejected', { what: 'devices', reason: 'sin-acta' })
|
|
634
|
+
return reply(from, { type: MSG.ERROR, error: 'unauthorized: this vault has no record to decide with' })
|
|
635
|
+
}
|
|
636
|
+
audit('devices', { device: await deviceIdOf(chk.device).catch(() => null), solo: 'revocaciones' })
|
|
637
|
+
return reply(from, { type: MSG.DEVICES_RESULT, devices: [], revoked, acta: record })
|
|
638
|
+
}
|
|
623
639
|
// El acta viaja con la lista: así cada dispositivo se entera de los cambios de
|
|
624
640
|
// política (quién manda, quién puede qué) sin un canal aparte.
|
|
625
|
-
const record = (await identity.profileActa?.().catch(() => null))?.acta || null
|
|
626
641
|
// Si el dispositivo estuvo apagado y viene con un `seq` viejo, se le manda la CADENA
|
|
627
642
|
// que falta (ventana de retención, §1.3) para que compruebe el encadenamiento en vez
|
|
628
643
|
// de tragarse un salto a ciegas. Si se salió de la ventana, llega vacía y toca
|
|
@@ -673,7 +688,6 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
673
688
|
// cert fresco (misma sub-clave y scope) sin QR ni aprobación — sigue siendo el
|
|
674
689
|
// mismo dispositivo enrolado, solo extiende la ventana. Un cert vencido o
|
|
675
690
|
// revocado NO puede renovarse (ahí sí toca re-emparejar con aprobación).
|
|
676
|
-
const RENEW_TTL_MS = 30 * 24 * 60 * 60 * 1000
|
|
677
691
|
async function handleRenew (from, p) {
|
|
678
692
|
if (!isFresh(p.data)) { audit('rejected', { what: 'renew', reason: 'stale' }); return staleReply(from) }
|
|
679
693
|
const chk = await verifyChain({ data: p.data, signature: p.signature, cert: p.cert, ...(await contextoActa()), revoked: await revocationSet() })
|
|
@@ -699,9 +713,22 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
699
713
|
audit('rejected', { what: 'renew', device: await deviceIdOf(p.cert.sub), reason: 'not-a-member' })
|
|
700
714
|
return reply(from, { type: MSG.ERROR, error: 'unauthorized: the record no longer lists this device' })
|
|
701
715
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
716
|
+
// RENOVAR NO RETIRA EL PAPEL ANTERIOR (`supersede: false`), y esto costó dos servicios.
|
|
717
|
+
//
|
|
718
|
+
// Retirarlo al emitir parece limpio —un aparato, un papel— pero convierte cualquier
|
|
719
|
+
// renovación que falle DESPUÉS de emitirse en una expulsión permanente: la máquina se
|
|
720
|
+
// queda con el papel viejo, que acaba de quedar revocado, y ya no puede ni pedir otro.
|
|
721
|
+
// Pasó dos veces en la migración del VPS (el registro de selladores y el bot social):
|
|
722
|
+
// el papel nuevo salió, la respuesta no llegó a guardarse, y al reintentar la bóveda
|
|
723
|
+
// contestaba `unauthorized: revoked`. Re-emparejar a mano, con TTY, por una renovación.
|
|
724
|
+
//
|
|
725
|
+
// Que convivan dos papeles del mismo aparato ya no es el problema que era: ninguno
|
|
726
|
+
// vence, pero lo que pueden hacer lo decide el ACTA en cada mostrador, así que el
|
|
727
|
+
// viejo no autoriza nada que el nuevo no autorice. Y quitar el aparato de verdad sigue
|
|
728
|
+
// siendo `revokeDevice`, que los retira TODOS de una vez.
|
|
729
|
+
const { cert } = await identity.signDelegation(p.cert.sub, scope, { label: prev?.label || '', supersede: false })
|
|
730
|
+
audit('renew', { device: await deviceIdOf(p.cert.sub), label: prev?.label || '', scope, seq: cert.seq })
|
|
731
|
+
log(`[vault] cert renewed for ${await deviceIdOf(p.cert.sub)} · record #${cert.seq}`)
|
|
705
732
|
// El acta viaja con el papel: sin ella quien lo recibe no puede comprobar que lo firmó
|
|
706
733
|
// una selladora del perfil, que es lo que sustituyó a «lo firmó la maestra».
|
|
707
734
|
reply(from, { type: MSG.RENEWED, cert, acta: record })
|