@dotrino/vaultd 0.80.2 → 0.81.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 +33 -5
- package/package.json +1 -1
- package/src/daemon.js +12 -1
- package/src/vault.js +172 -15
package/lib/src/service.js
CHANGED
|
@@ -666,7 +666,17 @@ async function openSealedBundle (sealed, ident, acta = null, masterPubkey = null
|
|
|
666
666
|
const gen = e.gen ?? e.e?.gen ?? 0
|
|
667
667
|
await comprobarFirma(e.owner, key, gen, e.e, e.seal)
|
|
668
668
|
const cek = await cekDe(cual, gen)
|
|
669
|
-
if (!cek)
|
|
669
|
+
if (!cek) {
|
|
670
|
+
// NO ES UN TROPIEZO: ES UN ESTADO QUE HAY QUE ARREGLAR EN LA BÓVEDA.
|
|
671
|
+
//
|
|
672
|
+
// Un aparato que entra después de escrita una variable no tiene envoltura de ella, y
|
|
673
|
+
// NO PUEDE tenerla: envolver exige abrir la llave del cajón, y eso solo pasa cuando
|
|
674
|
+
// el dueño abre la bóveda. Reintentar no la crea. Lleva `code` porque el mensaje lo
|
|
675
|
+
// empareja otra capa y una frase no es un contrato (ver `dotrino-error-strings`).
|
|
676
|
+
throw Object.assign(
|
|
677
|
+
new Error(`no key to open ${key}: this device has no wrapping in its drawer — open the vault (dotrino-vault unlock) so it hands it the key`),
|
|
678
|
+
{ code: 'no-wrapping' })
|
|
679
|
+
}
|
|
670
680
|
out[key] = await decryptWithCek({ cek, envelope: e.e })
|
|
671
681
|
}
|
|
672
682
|
return out
|
|
@@ -991,16 +1001,34 @@ function fingerprintOf (secrets) {
|
|
|
991
1001
|
* servicio no opera hasta que esto resuelva — esa es la regla.
|
|
992
1002
|
* @returns {Promise<Record<string,string>>}
|
|
993
1003
|
*/
|
|
1004
|
+
/**
|
|
1005
|
+
* ¿ESTE FALLO SE ARREGLA SOLO ESPERANDO? Si no, reintentar es peor que rendirse.
|
|
1006
|
+
*
|
|
1007
|
+
* Lo NO transitorio exige que alguien haga algo: falta de enrolamiento, cert revocado o
|
|
1008
|
+
* vencido, scope equivocado → re-emparejar. Que te lo DENIEGUEN tampoco se reintenta: fue
|
|
1009
|
+
* una decisión, no un tropiezo.
|
|
1010
|
+
*
|
|
1011
|
+
* Y no tener ENVOLTURA en el cajón (`no-wrapping`) es el que costó una tarde. La bóveda
|
|
1012
|
+
* pedía aprobación, el teléfono timbraba, el dueño aprobaba, la bóveda contestaba un bundle
|
|
1013
|
+
* que este aparato no podía abrir, y el agente volvía a pedir — así que el teléfono
|
|
1014
|
+
* timbraba otra vez. «Sigo aprobando y aprobando», textual. Ninguna aprobación iba a crear
|
|
1015
|
+
* esa envoltura: envolver exige abrir la llave del cajón, o sea ABRIR la bóveda.
|
|
1016
|
+
*
|
|
1017
|
+
* Se decide por `e.code` donde lo hay: emparejar un mensaje por su texto se rompe en
|
|
1018
|
+
* silencio en cuanto alguien lo reescribe (ver `dotrino-error-strings`).
|
|
1019
|
+
*/
|
|
1020
|
+
export function isFinal (e) {
|
|
1021
|
+
if (e?.code === 'no-wrapping') return true
|
|
1022
|
+
return /not enrolled|invalid ns|unauthorized: (revoked|expired|scope|cn|untrusted-issuer|cert-device-mismatch|denied)/.test(e?.message || '')
|
|
1023
|
+
}
|
|
1024
|
+
|
|
994
1025
|
export async function waitForSecrets ({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, retryMs = 5000, maxRetryMs = 60000, onRetry, onPending, onCert } = {}) {
|
|
995
1026
|
let delay = retryMs
|
|
996
1027
|
for (;;) {
|
|
997
1028
|
try {
|
|
998
1029
|
return await fetchSecrets({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, onPending, onCert })
|
|
999
1030
|
} catch (e) {
|
|
1000
|
-
|
|
1001
|
-
// cert revocado/vencido o scope equivocado exigen re-emparejar → se corta.
|
|
1002
|
-
// Que te lo DENIEGUEN tampoco se reintenta: fue una decisión, no un tropiezo.
|
|
1003
|
-
if (/not enrolled|invalid ns|unauthorized: (revoked|expired|scope|cn|untrusted-issuer|cert-device-mismatch|denied)/.test(e.message)) throw e
|
|
1031
|
+
if (isFinal(e)) throw e
|
|
1004
1032
|
try { onRetry?.(e, delay) } catch (_) {}
|
|
1005
1033
|
await new Promise((r) => setTimeout(r, delay))
|
|
1006
1034
|
delay = Math.min(maxRetryMs, Math.round(delay * 1.6))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.81.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
|
@@ -332,7 +332,18 @@ export async function runDaemon () {
|
|
|
332
332
|
r.wrapped, r.drawers, r.dropped ? `, ${r.dropped} stale one(s) dropped` : '')
|
|
333
333
|
if (r?.dropped) note = ` · llavero al día (${r.dropped} envoltura(s) de más retirada(s))`
|
|
334
334
|
else if (r?.wrapped) note = ' · llavero al día'
|
|
335
|
-
|
|
335
|
+
// ABIERTA PERO SIN LLAVERO NO ES «ABIERTA». Si un cajón no se pudo reenvolver, sus
|
|
336
|
+
// aparatos no van a poder leer nada y hay que decirlo AQUÍ: contestar
|
|
337
|
+
// «desbloqueado» a secas dejaba la bóveda pareciendo sana, y el fallo aparecía
|
|
338
|
+
// días después como un servicio pidiendo sus claves en bucle.
|
|
339
|
+
if (r?.failed?.length) {
|
|
340
|
+
for (const f of r.failed) console.error('[vault] %s: could not reseal (%s)', f.owner, f.error)
|
|
341
|
+
note += ` · ⚠ ${r.failed.length} cajón(es) NO se pudieron reenvolver (${r.failed.map((f) => f.owner).join(', ')}): sus aparatos no podrán leerlos`
|
|
342
|
+
}
|
|
343
|
+
} catch (e) {
|
|
344
|
+
console.error('[vault] could not rebuild the keyring on unlock:', e.message)
|
|
345
|
+
note += ` · ⚠ el llavero NO se pudo rehacer (${e.message}): los aparatos no podrán leer sus cajones`
|
|
346
|
+
}
|
|
336
347
|
finally { wipe(ak) }
|
|
337
348
|
return { done: 'perfil desbloqueado' + note, autoLockMs: mgr.profiles.autoLockMs }
|
|
338
349
|
}
|
package/src/vault.js
CHANGED
|
@@ -798,9 +798,19 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
798
798
|
await identity.setMemberEncPub({ pub: chk.device, encPub: p.data.encPub })
|
|
799
799
|
audit('enckey', { device: await deviceIdOf(chk.device).catch(() => null), ns })
|
|
800
800
|
log(`[vault] ${ns}: encryption key registered for ${await deviceIdOf(chk.device).catch(() => '????-????')}`)
|
|
801
|
-
// Ya puede recibir sobres: se le envuelve la llave de
|
|
801
|
+
// Ya puede recibir sobres: se le envuelve la llave de sus cajones en el acto, o
|
|
802
802
|
// seguiría sin poder abrir nada hasta la siguiente escritura.
|
|
803
|
-
|
|
803
|
+
//
|
|
804
|
+
// Va por `recipientsOf` —el único sitio que sabe quién debe tener envoltura de un
|
|
805
|
+
// cajón—, que incluye a los SELLADORES. Con `nsMembers` se envolvía solo para los del
|
|
806
|
+
// ns y los selladores se quedaban con lo viejo (dueño, 2026-09-01).
|
|
807
|
+
//
|
|
808
|
+
// Y su cajón PROPIO también: un aparato con variables suyas se quedaba sin poder
|
|
809
|
+
// abrirlas, que es el mismo agujero por otra puerta.
|
|
810
|
+
for (const owner of [`ns:${ns}`, `dev:${chk.device}`]) {
|
|
811
|
+
await spreadKey(owner, await recipientsOf(owner))
|
|
812
|
+
.catch((e) => log(`[vault] ${owner}: could not hand it the key: ${e.message}`))
|
|
813
|
+
}
|
|
804
814
|
const body = { op: 'secrets.result', ns, enc: null, ok: true, ts: Date.now() }
|
|
805
815
|
const { signature } = await identity.signData(body)
|
|
806
816
|
reply(from, { type: MSG.SECRETS_RESULT, body, signature })
|
|
@@ -881,6 +891,20 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
881
891
|
const payload = b.legacy
|
|
882
892
|
? { secrets: Object.fromEntries(Object.entries(b.entries).map(([k, e]) => [k, e.v])), acta: record || null }
|
|
883
893
|
: { sealed: b, acta: record || null }
|
|
894
|
+
// LO QUE SE MANDA CON AGUJEROS, SE DICE. La bóveda sabe aquí mismo que este aparato no
|
|
895
|
+
// tiene envoltura de tal variable —`missingFor` lo contesta—, y hasta hoy se callaba:
|
|
896
|
+
// contestaba un bundle que el agente no podía abrir y el registro decía «aprobado»,
|
|
897
|
+
// como si se hubiera servido. Desde fuera parecía que el pedido funcionaba y volvía a
|
|
898
|
+
// aparecer una y otra vez. Falta la envoltura, y eso solo se arregla ABRIENDO la
|
|
899
|
+
// bóveda; decirlo aquí es lo que convierte un bucle mudo en algo que se puede arreglar.
|
|
900
|
+
if (!b.legacy) {
|
|
901
|
+
const faltan = [...secrets.missingFor(`ns:${ns}`, devicePub), ...secrets.missingFor(`dev:${devicePub}`, devicePub)]
|
|
902
|
+
if (faltan.length) {
|
|
903
|
+
const quien = await deviceIdOf(devicePub).catch(() => '????-????')
|
|
904
|
+
audit('secrets.no-wrapping', { device: quien, ns, vars: faltan })
|
|
905
|
+
log(`[vault] ${ns}: ${quien} has no wrapping for ${faltan.join(', ')} — open the vault (dotrino-vault unlock) so it hands it the key`)
|
|
906
|
+
}
|
|
907
|
+
}
|
|
884
908
|
const enc = await seal({ ek, payload })
|
|
885
909
|
const body = { op: 'secrets.result', ns, enc, ts: Date.now() }
|
|
886
910
|
// LA MAESTRA NO FIRMA ESTO. Su trabajo es sellar el acta y reenvolver sobres; servir
|
|
@@ -943,7 +967,8 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
943
967
|
if (op === 'deny') {
|
|
944
968
|
audit('secrets.denied', { device: pend.deviceId, ns: pend.ns, id, by })
|
|
945
969
|
log(`[vault] ${pend.ns}: request of ${pend.deviceId} DENIED by ${by}`)
|
|
946
|
-
try { client.sendByPubkey(pend.device, { type: MSG.ERROR, error: `unauthorized: denied — the "${pend.ns}" request was denied from ${by}` }) }
|
|
970
|
+
try { client.sendByPubkey(pend.device, { type: MSG.ERROR, error: `unauthorized: denied — the "${pend.ns}" request was denied from ${by}` }) }
|
|
971
|
+
catch (e) { log(`[vault] ${pend.ns}: could not tell ${pend.deviceId} it was denied: ${e.message}`) }
|
|
947
972
|
return answer({ op: 'deny.result', id, ok: true })
|
|
948
973
|
}
|
|
949
974
|
if (op !== 'approve') return reply(from, { type: MSG.ERROR, error: 'approval: unknown op' })
|
|
@@ -954,18 +979,42 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
954
979
|
audit('secrets.approved', { device: pend.deviceId, ns: pend.ns, id, by })
|
|
955
980
|
log(`[vault] ${pend.ns}: request of ${pend.deviceId} approved by ${by}`)
|
|
956
981
|
// Va por `sendByPubkey`: si el que pedía ya no está conectado, lo recoge al volver.
|
|
957
|
-
|
|
982
|
+
// EL FALLO NO SE TRAGA. Tragarlo era lo peor de este mostrador: la bóveda apuntaba
|
|
983
|
+
// «aprobado», el que pedía no recibía nada, y no quedaba una sola línea que mirar —
|
|
984
|
+
// así que el dueño aprobaba una y otra vez creyendo que el teléfono no llegaba.
|
|
985
|
+
try { client.sendByPubkey(pend.device, { type: MSG.SECRETS_RESULT, ...res }) }
|
|
986
|
+
catch (e) { log(`[vault] ${pend.ns}: approved, but the reply did not reach ${pend.deviceId}: ${e.message}`) }
|
|
958
987
|
return answer({ op: 'approve.result', id, ok: true })
|
|
959
988
|
}
|
|
960
989
|
|
|
961
|
-
/**
|
|
990
|
+
/**
|
|
991
|
+
* Aviso a los aparatos que aprueban (cola del proxio → timbre nativo si están apagados).
|
|
992
|
+
*
|
|
993
|
+
* LA MAESTRA NO FIRMA ESTO, y es el fallo que dejaba el mecanismo entero inservible.
|
|
994
|
+
* Avisar es SERVIR, y servir es trabajo de la llave de sellado —la maestra solo sella el
|
|
995
|
+
* acta y reenvuelve los sobres al abrir (CLAUDE.md, «la maestra tiene DOS trabajos»)—.
|
|
996
|
+
* Con `identity.signData` esto reventaba con «vault locked» en cada pedido, o sea SIEMPRE:
|
|
997
|
+
* la bóveda vive cerrada, que es justamente para lo que sirve el candado. Y como reventaba
|
|
998
|
+
* antes de mandar nada, el proxio no tenía a quién encolar y el teléfono NO TIMBRABA
|
|
999
|
+
* NUNCA. El pedido solo se veía si al dueño se le ocurría abrir la app por su cuenta.
|
|
1000
|
+
*
|
|
1001
|
+
* El aviso no lleva nada que no esté ya en el pedido, y el timbre que sale de aquí sigue
|
|
1002
|
+
* sin contenido: quien lo recibe pregunta por la lista, no la lee del aviso.
|
|
1003
|
+
*/
|
|
962
1004
|
async function notifyApprovers (pend, record) {
|
|
963
1005
|
try {
|
|
964
1006
|
const body = { ev: 'approval', id: pend.id, ns: pend.ns, deviceId: pend.deviceId, label: pend.label, exp: pend.exp, ts: Date.now() }
|
|
965
|
-
const
|
|
1007
|
+
const seal = await sealOrFail(body)
|
|
966
1008
|
const who = (record?.members || []).filter((m) => Acta.memberCan(record, m.pub, 'approve')).map((m) => m.pub)
|
|
967
1009
|
if (!who.length) log(`[vault] ${pend.ns}: nobody can approve (grant it with: dotrino-vault caps <ID> +aprueba)`)
|
|
968
|
-
|
|
1010
|
+
// EL FALLO NO SE TRAGA. Un aviso que no sale es un teléfono que no timbra, y eso
|
|
1011
|
+
// desde fuera se ve igual que «nadie ha aprobado todavía»: se espera para siempre
|
|
1012
|
+
// sin nada que mirar. Es el mismo cuidado que en `notifyMembers`.
|
|
1013
|
+
for (const pub of who) {
|
|
1014
|
+
try { client.sendByPubkey(pub, { type: MSG.ADMIN_EVENT, body, seal }) }
|
|
1015
|
+
catch (e) { log(`[vault] could not ring ${pub.slice(0, 24)}… about ${pend.ns}: ${e.message}`) }
|
|
1016
|
+
}
|
|
1017
|
+
log(`[vault] ${pend.ns}: rang ${who.length} approver(s) for ${pend.id}`)
|
|
969
1018
|
} catch (e) { log('[vault] could not notify approvers:', e.message) }
|
|
970
1019
|
}
|
|
971
1020
|
|
|
@@ -1750,7 +1799,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1750
1799
|
*/
|
|
1751
1800
|
async function setSecret (ns, key, value, isPublic, { by = null } = {}) {
|
|
1752
1801
|
await secrets.set(ns, key, value, isPublic, { by })
|
|
1753
|
-
await settleDebts(`ns:${ns}
|
|
1802
|
+
await settleDebts(`ns:${ns}`)
|
|
1754
1803
|
audit('secret.set', { ns, key }); scheduleNotice(ns)
|
|
1755
1804
|
}
|
|
1756
1805
|
|
|
@@ -1815,8 +1864,79 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1815
1864
|
return { done, asked: pending.length }
|
|
1816
1865
|
}
|
|
1817
1866
|
|
|
1867
|
+
/**
|
|
1868
|
+
* REHACER EL LLAVERO ENTERO. Se llama al ABRIR la bóveda, que es el único momento en que
|
|
1869
|
+
* se puede: envolver exige la llave del cajón, y esa solo se abre con la frase.
|
|
1870
|
+
*
|
|
1871
|
+
* Lo que falle aquí SALE en `failed`, y quien llame tiene que decirlo. Callarlo fue el
|
|
1872
|
+
* fallo: un cajón que no se pudo reenvolver deja a sus aparatos sin poder leer nada, y
|
|
1873
|
+
* desde fuera la bóveda parecía abierta y sana — «desbloqueado» a secas—. Se descubría
|
|
1874
|
+
* días después, por un servicio que pedía sus claves una y otra vez.
|
|
1875
|
+
*/
|
|
1876
|
+
/**
|
|
1877
|
+
* QUE LOS SOBRES EXISTAN PARA TODOS, SIEMPRE — TAMBIÉN AL SELLAR EL ACTA.
|
|
1878
|
+
*
|
|
1879
|
+
* Regla del dueño (2026-09-01): «cuando se abre el vault, y siempre, debe asegurarse de
|
|
1880
|
+
* que todos los sobres para todos los dispositivos existan, antes y después de sellar un
|
|
1881
|
+
* acta; es la tarea de la llave maestra, no tiene otra».
|
|
1882
|
+
*
|
|
1883
|
+
* Y es literalmente su otro trabajo: la maestra firma el acta y regenera los sobres. Todo
|
|
1884
|
+
* cambio del acta cambia QUIÉN debe tener envoltura de qué —entra un aparato, se le da
|
|
1885
|
+
* `secrets`, se le quita, aparece un sellador nuevo—, así que sellar sin repasar los
|
|
1886
|
+
* sobres deja el llavero diciendo una cosa y el acta otra. Eso fue justo el agujero: un
|
|
1887
|
+
* servicio en el acta, con su permiso, y sin poder abrir nada.
|
|
1888
|
+
*
|
|
1889
|
+
* Con la bóveda ABIERTA se rehace de verdad. Cerrada no se puede —envolver exige la llave
|
|
1890
|
+
* del cajón— y entonces queda como DEUDA anotada, que es lo que salda el `unlock`. Lo que
|
|
1891
|
+
* no se hace es callarlo.
|
|
1892
|
+
*/
|
|
1893
|
+
/**
|
|
1894
|
+
* QUE LOS SOBRES EXISTAN PARA TODOS, CADA VEZ QUE EL ACTA CAMBIA.
|
|
1895
|
+
*
|
|
1896
|
+
* Regla del dueño (2026-09-01): «cuando se abre el vault, y siempre, debe asegurarse de
|
|
1897
|
+
* que todos los sobres para todos los dispositivos existan, antes y después de sellar un
|
|
1898
|
+
* acta; es la tarea de la llave maestra, no tiene otra». Y es literalmente su otro
|
|
1899
|
+
* trabajo: firmar el acta y regenerar los sobres.
|
|
1900
|
+
*
|
|
1901
|
+
* SE REHACE ENTERO, no se salda lo que falta. Saldar añade lo que no está pero no MIRA lo
|
|
1902
|
+
* que sobra: la envoltura de alguien a quien ya se le quitó el permiso se quedaría ahí
|
|
1903
|
+
* para siempre y nadie se enteraría. `resealAll` con `exact` deja cada cajón envuelto para
|
|
1904
|
+
* exactamente quien dice el acta —crea lo que falta y RETIRA lo caduco—, que es la única
|
|
1905
|
+
* forma de poder afirmar que el llavero está al día. No toca la red: es reenvolver con
|
|
1906
|
+
* las públicas del acta, así que esperar aquí no cuelga de ningún aparato apagado.
|
|
1907
|
+
*
|
|
1908
|
+
* NO HAY CAMINO PARA «LA BÓVEDA ESTÁ CERRADA» (dueño, 2026-09-01: «es un absurdo, con
|
|
1909
|
+
* bóveda cerrada no puede cambiar el acta»). Sellar el acta necesita la maestra, y cerrada
|
|
1910
|
+
* la maestra no está en memoria — o sea que si se llegó hasta aquí, estaba abierta. Un
|
|
1911
|
+
* `if` para ese caso no sería prudencia: sería un repliegue que le pone cara de trámite
|
|
1912
|
+
* normal a una contradicción. Si aun así falta la llave, se dice como lo que es.
|
|
1913
|
+
*/
|
|
1914
|
+
async function refreshWraps (motivo) {
|
|
1915
|
+
// HAY CANDADO SOLO SI HAY LAS DOS COSAS: contraseña Y alguien que sepa abrirla.
|
|
1916
|
+
//
|
|
1917
|
+
// `startVault` sin gestor de perfiles trae `hasPassword = () => true` y `openKey =
|
|
1918
|
+
// null`, que leído a la letra dice «tiene contraseña pero no hay con qué abrirla» — el
|
|
1919
|
+
// estado imposible. Mirando solo `hasPassword` se daba por cerrada una bóveda que abre
|
|
1920
|
+
// con la llave de la máquina, y el repaso del llavero se saltaba ENTERO: el servicio se
|
|
1921
|
+
// quedaba sin su envoltura y contestábamos un paquete que no podía abrir. Es el mismo
|
|
1922
|
+
// fallo que se está arreglando, colado por la puerta de al lado.
|
|
1923
|
+
const conCandado = typeof openKey === 'function' && (() => { try { return !!hasPassword() } catch (_) { return true } })()
|
|
1924
|
+
const k = conCandado ? openKey() : null
|
|
1925
|
+
if (conCandado && !k) {
|
|
1926
|
+
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`)
|
|
1927
|
+
return null
|
|
1928
|
+
}
|
|
1929
|
+
const r = await resealAll(k).catch((e) => { log(`[vault] ${motivo}: could not rebuild the keyring: ${e.message}`); return null })
|
|
1930
|
+
if (r?.wrapped) log(`[vault] ${motivo}: keyring rebuilt (${r.wrapped} wrap(s) in ${r.drawers} drawer(s))`)
|
|
1931
|
+
// LOS SOBRES CADUCOS SE DICEN. Son envolturas de quien ya no debería poder abrir ese
|
|
1932
|
+
// cajón: retirarlas en silencio esconde que alguien las tuvo de más.
|
|
1933
|
+
if (r?.dropped) log(`[vault] ${motivo}: ${r.dropped} stale wrap(s) dropped (they belonged to members the record no longer names)`)
|
|
1934
|
+
if (r?.failed?.length) log(`[vault] ${motivo}: ${r.failed.length} drawer(s) could NOT be resealed: ${r.failed.map((f) => f.owner).join(', ')}`)
|
|
1935
|
+
return r
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1818
1938
|
async function resealAll (adminKey = null) {
|
|
1819
|
-
const out = { drawers: 0, wrapped: 0, dropped: 0, sinLlave: [] }
|
|
1939
|
+
const out = { drawers: 0, wrapped: 0, dropped: 0, sinLlave: [], failed: [] }
|
|
1820
1940
|
for (const owner of secrets.owners?.() || []) {
|
|
1821
1941
|
const before = new Set(secrets.recipientsIn(owner))
|
|
1822
1942
|
const members = await recipientsOf(owner)
|
|
@@ -1829,9 +1949,11 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1829
1949
|
for (const p of r?.sinLlave || []) out.sinLlave.push(p)
|
|
1830
1950
|
} catch (e) {
|
|
1831
1951
|
log(`[vault] ${owner}: could not reseal (${e.message})`)
|
|
1952
|
+
out.failed.push({ owner, error: e.message })
|
|
1832
1953
|
}
|
|
1833
1954
|
}
|
|
1834
1955
|
if (out.dropped) audit('secret.reseal', { drawers: out.drawers, dropped: out.dropped })
|
|
1956
|
+
if (out.failed.length) audit('secret.reseal-failed', { drawers: out.failed.map((f) => f.owner) })
|
|
1835
1957
|
return out
|
|
1836
1958
|
}
|
|
1837
1959
|
|
|
@@ -1871,12 +1993,20 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1871
1993
|
* anotado, que es lo que las listas enseñan. No se propaga el error: la variable YA se
|
|
1872
1994
|
* guardó, y el que escribe no tiene por qué enterarse de una deuda vieja.
|
|
1873
1995
|
*/
|
|
1874
|
-
async function settleDebts (owner
|
|
1996
|
+
async function settleDebts (owner) {
|
|
1875
1997
|
const ns = owner.startsWith('ns:') ? owner.slice(3) : null
|
|
1876
1998
|
const owed = (ns && store.getSetting(`rotate-due:${ns}`)) ||
|
|
1877
1999
|
(await incompleteMembers()).some((m) => m.owners[owner])
|
|
1878
2000
|
if (!owed) return null
|
|
1879
|
-
|
|
2001
|
+
// QUIÉN RECIBE ENVOLTURA LO DICE `recipientsOf`, Y SOLO ÉL.
|
|
2002
|
+
//
|
|
2003
|
+
// Antes cada llamador traía su propia lista (`nsMembers`, o el miembro suelto), y todas
|
|
2004
|
+
// se dejaban fuera a los SELLADORES — que `recipientsOf` sí incluye. Resultado: escribir
|
|
2005
|
+
// una variable envolvía para el servicio y dejaba al sellador con la generación vieja
|
|
2006
|
+
// (dueño, 2026-09-01: «al envolver hay que envolver siempre para todos los selladores,
|
|
2007
|
+
// para que siempre tengan la info fresca»). Dos listas para la misma pregunta acaban
|
|
2008
|
+
// siempre así: una se actualiza y la otra no.
|
|
2009
|
+
try { return await spreadKey(owner, await recipientsOf(owner), null) } catch (_) { return null }
|
|
1880
2010
|
}
|
|
1881
2011
|
async function deleteSecret (ns, key) { const ok = await secrets.delete(ns, key); if (ok) { audit('secret.rm', { ns, key }); scheduleNotice(ns) } return ok }
|
|
1882
2012
|
|
|
@@ -1914,7 +2044,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1914
2044
|
}
|
|
1915
2045
|
})
|
|
1916
2046
|
if (changed.length) {
|
|
1917
|
-
await settleDebts(`ns:${ns}
|
|
2047
|
+
await settleDebts(`ns:${ns}`)
|
|
1918
2048
|
scheduleNotice(ns)
|
|
1919
2049
|
}
|
|
1920
2050
|
return changed
|
|
@@ -1936,7 +2066,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1936
2066
|
}
|
|
1937
2067
|
})
|
|
1938
2068
|
if (changed.length) {
|
|
1939
|
-
await settleDebts(`dev:${pub}
|
|
2069
|
+
await settleDebts(`dev:${pub}`)
|
|
1940
2070
|
const device = await deviceIdOf(pub).catch(() => null)
|
|
1941
2071
|
for (const it of list) {
|
|
1942
2072
|
if (!changed.includes(it.key)) continue
|
|
@@ -2015,7 +2145,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2015
2145
|
async function setDeviceSecret (pub, key, value, isPublic, { by = null } = {}) {
|
|
2016
2146
|
const m = await requireService(pub)
|
|
2017
2147
|
await secrets.setDevice(pub, key, value, isPublic, { by })
|
|
2018
|
-
await settleDebts(`dev:${pub}
|
|
2148
|
+
await settleDebts(`dev:${pub}`)
|
|
2019
2149
|
audit('secret.set', { device: await deviceIdOf(pub).catch(() => null), ns: m?.cn || null, key, scope: 'device' })
|
|
2020
2150
|
scheduleDeviceNotice(pub)
|
|
2021
2151
|
}
|
|
@@ -2107,7 +2237,12 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2107
2237
|
// el archivo vendorizado en el iframe de identidad.
|
|
2108
2238
|
const m = r?.cert?.sub ? await memberOf(r.cert.sub) : null
|
|
2109
2239
|
if (m?.cn) {
|
|
2110
|
-
|
|
2240
|
+
// POR `recipientsOf`, NO por `nsMembers`. Quien debe tener envoltura de un cajón lo
|
|
2241
|
+
// dice UN sitio, y ahí dentro están también los SELLADORES (dueño, 2026-09-01: «al
|
|
2242
|
+
// envolver hay que envolver siempre para todos los selladores, para que siempre
|
|
2243
|
+
// tengan la info fresca»). Repartiendo solo a los del ns, un cajón tocado por esta
|
|
2244
|
+
// puerta dejaba a los selladores con la envoltura vieja.
|
|
2245
|
+
await spreadKey(`ns:${m.cn}`, await recipientsOf(`ns:${m.cn}`), adminKey).catch(async (e) => {
|
|
2111
2246
|
log('[vault] could not hand the key to the new service:', e.message)
|
|
2112
2247
|
// Sin la frase la bóveda no puede envolvérsela… pero un HERMANO suyo sí: otro
|
|
2113
2248
|
// servicio del mismo cajón ya tiene la llave abierta (§8.11). Si contesta, el
|
|
@@ -2117,6 +2252,9 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2117
2252
|
if (!r2.done) log(`[vault] ns:${m.cn}: nobody could hand it the key — it stays in debt until the vault is opened`)
|
|
2118
2253
|
})
|
|
2119
2254
|
}
|
|
2255
|
+
// Y el llavero ENTERO: el recién llegado también es destinatario de su propio cajón,
|
|
2256
|
+
// y si entra con `sella` lo es de TODOS. Repartir solo el suyo dejaba el resto atrás.
|
|
2257
|
+
await refreshWraps('enrolled')
|
|
2120
2258
|
await notifyMembers('enrolled', { deviceId: r?.deviceId || null, by: 'pc' })
|
|
2121
2259
|
return r
|
|
2122
2260
|
},
|
|
@@ -2161,6 +2299,16 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2161
2299
|
// rotar su llave). Lo enseñan `secret list` y la consola: si no se ve, no se salda.
|
|
2162
2300
|
rotationsDue,
|
|
2163
2301
|
secretDebts,
|
|
2302
|
+
/**
|
|
2303
|
+
* QUIÉN tiene envoltura de la generación vigente de un cajón (llaves de firma, más
|
|
2304
|
+
* `#recovery`). Es diagnóstico: saber a cuántos se les envolvió no ayuda a abrir nada,
|
|
2305
|
+
* y en cambio es lo único que contesta de verdad a «¿quién puede leer esto?».
|
|
2306
|
+
*
|
|
2307
|
+
* Se expone porque hacía falta para COMPROBARLO en una prueba en vez de argumentarlo:
|
|
2308
|
+
* la condición del dueño para acotar los sobres por cajón y permisos —en vez de
|
|
2309
|
+
* envolver para todos— fue estar seguro de que no falta ninguno.
|
|
2310
|
+
*/
|
|
2311
|
+
secretRecipients: (owner) => secrets.recipientsIn(owner),
|
|
2164
2312
|
// ¿Es ESTA bóveda la que sella el acta? Lo usa el freno de borrado (D12).
|
|
2165
2313
|
isMaster: () => identity.isMaster(),
|
|
2166
2314
|
setCaps: async (pub, caps) => {
|
|
@@ -2178,6 +2326,9 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2178
2326
|
}
|
|
2179
2327
|
} catch (_) { /* comprobar es un extra: si falla, no rompe el cambio */ }
|
|
2180
2328
|
audit('caps', { device: await deviceIdOf(pub).catch(() => null), caps })
|
|
2329
|
+
// El acta acaba de cambiar QUIÉN debe tener envoltura de qué: se repasa antes de
|
|
2330
|
+
// avisar, para que el aviso salga con el llavero ya al día.
|
|
2331
|
+
await refreshWraps('caps')
|
|
2181
2332
|
await notifyMembers('caps', { deviceId: await deviceIdOf(pub).catch(() => null), caps })
|
|
2182
2333
|
return r
|
|
2183
2334
|
},
|
|
@@ -2193,6 +2344,11 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2193
2344
|
// QUITAR EL DISPOSITIVO. Se identifica por su llave (`sub`), no por un `nonce`: un
|
|
2194
2345
|
// aparato puede tener varios certificados y retirar uno no lo echaba de la bóveda.
|
|
2195
2346
|
// Se sigue aceptando `{ nonce }` para no romper una consola vieja en vuelo.
|
|
2347
|
+
//
|
|
2348
|
+
// Al salir alguien el acta cambia igual que al entrar, así que el llavero se repasa
|
|
2349
|
+
// también aquí: lo que sobra se quita (`resealAll` lo hace) y el que se queda sigue
|
|
2350
|
+
// completo. Un llavero que solo se repasa al añadir acumula envolturas de quien ya no
|
|
2351
|
+
// está, que es lo contrario de lo que se quiso al expulsarlo.
|
|
2196
2352
|
revokeDevice: async (target) => {
|
|
2197
2353
|
const sub = typeof target === 'object' && target ? target.sub : null
|
|
2198
2354
|
if (!sub) {
|
|
@@ -2203,6 +2359,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2203
2359
|
}
|
|
2204
2360
|
const r = await desk.revokeDevice(sub)
|
|
2205
2361
|
audit('revoke-device', { device: await deviceIdOf(sub).catch(() => null), certs: r?.nonces?.length ?? null })
|
|
2362
|
+
await refreshWraps('revoked')
|
|
2206
2363
|
await notifyMembers('revoked', { deviceId: await deviceIdOf(sub).catch(() => null), by: 'pc' })
|
|
2207
2364
|
return r
|
|
2208
2365
|
},
|