@dotrino/vaultd 0.147.0 → 0.148.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 +60 -6
- package/package.json +1 -1
- package/src/daemon.js +36 -4
- package/src/notices.js +14 -7
- package/src/updateApproval.js +29 -0
- package/src/vault.js +22 -13
package/lib/src/service.js
CHANGED
|
@@ -707,28 +707,82 @@ export async function askUpdateApproval ({ product, version, from = null, dir, p
|
|
|
707
707
|
* @param {object} o `product`, `version`, `from` y la conexión, como en `askUpdateApproval`
|
|
708
708
|
* @returns {Promise<{ ok: true }>}
|
|
709
709
|
*/
|
|
710
|
-
export
|
|
710
|
+
export function reportUpdated (o = {}) { return reportToVault('update-done', o) }
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* HAY VERSIÓN NUEVA Y NO PUEDO INSTALARLA SOLO: necesita permisos de administrador (el
|
|
714
|
+
* servicio está instalado para todo el sistema, no como usuario). El aparato se lo dice a su
|
|
715
|
+
* bóveda y ella avisa a quien aprueba, que es quien puede ir a esa máquina e instalarla a
|
|
716
|
+
* mano (dueño, 2026-10-08). No instala nada ni pide permiso: solo avisa.
|
|
717
|
+
*
|
|
718
|
+
* Mismos parámetros y errores que `reportUpdated`. Avisar una sola vez por versión es cosa
|
|
719
|
+
* de quien llama.
|
|
720
|
+
* @returns {Promise<{ ok: true }>}
|
|
721
|
+
*/
|
|
722
|
+
export function reportUpdateNeedsRoot (o = {}) { return reportToVault('update-needs-root', o) }
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* LOS TRES GANCHOS DE ACTUALIZACIÓN, YA ARMADOS para pasárselos a `watchSelfUpdateNpm` de
|
|
726
|
+
* `@dotrino/update/npm`: una pieza, para que cada agente no lo escriba por su cuenta. No
|
|
727
|
+
* depende de ese paquete: solo devuelve funciones con la forma que él espera.
|
|
728
|
+
*
|
|
729
|
+
* · `mayUpdate({ version, from })` → pregunta a la bóveda y devuelve si se puede. Si la
|
|
730
|
+
* bóveda NO CONTESTA, lanza: para quien llama eso es «no pude preguntar» y se reintenta.
|
|
731
|
+
* Si el pedido VENCE sin respuesta devuelve `false`: vencido es no, y no se repite.
|
|
732
|
+
* · `onUpdated({ version, from })` → le dice a la bóveda que se actualizó. Deja pasar el
|
|
733
|
+
* error, para que quien llama conserve su marcador y lo reintente.
|
|
734
|
+
* · `onNeedsRoot({ version, from })` → le dice que hay versión nueva y necesita permisos de
|
|
735
|
+
* administrador. Igual.
|
|
736
|
+
*
|
|
737
|
+
* @param {object} o
|
|
738
|
+
* @param {string} o.product nombre npm de la pieza
|
|
739
|
+
* @param {(m: string) => void} [o.log]
|
|
740
|
+
* …y la conexión como en los demás: `dir`, o `proxyUrl` + `masterPubkey` + `device` + `cert`.
|
|
741
|
+
* @returns {{ mayUpdate: (u: { version: string, from?: string }) => Promise<boolean>,
|
|
742
|
+
* onUpdated: (u: { version: string, from?: string }) => Promise<void>,
|
|
743
|
+
* onNeedsRoot: (u: { version: string, from?: string }) => Promise<void> }}
|
|
744
|
+
*/
|
|
745
|
+
export function vaultUpdateHooks ({ product, log = () => {}, dir, proxyUrl, masterPubkey, device, cert, _ask = askUpdateApproval, _done = reportUpdated, _root = reportUpdateNeedsRoot } = {}) {
|
|
746
|
+
if (typeof product !== 'string' || !product) throw new Error('vaultUpdateHooks: needs product')
|
|
747
|
+
const conn = { dir, proxyUrl, masterPubkey, device, cert }
|
|
748
|
+
return {
|
|
749
|
+
async mayUpdate ({ version, from = null } = {}) {
|
|
750
|
+
let r
|
|
751
|
+
try { r = await _ask({ ...conn, product, version, from, onPending: () => log(`[update] ${product} ${version}: waiting for approval on your vault`) }) } catch (e) {
|
|
752
|
+
if (e?.code === 'unanswered') { log(`[update] ${product} ${version}: nobody answered in time, so it is a no`); return false }
|
|
753
|
+
throw e
|
|
754
|
+
}
|
|
755
|
+
log(`[update] ${product} ${version}: ${r.ok ? (r.asked ? 'approved' : 'no approval needed') : 'not approved'}`)
|
|
756
|
+
return r.ok === true
|
|
757
|
+
},
|
|
758
|
+
async onUpdated ({ version, from = null } = {}) { await _done({ ...conn, product, version, from }) },
|
|
759
|
+
async onNeedsRoot ({ version, from = null } = {}) { await _root({ ...conn, product, version, from }) }
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** Lo común de los dos avisos: firmar, mandar, y comprobar quién contesta. */
|
|
764
|
+
async function reportToVault (op, { product, version, from = null, dir, proxyUrl, masterPubkey, device, cert, timeoutMs = 30000 } = {}) {
|
|
711
765
|
const saved = dir ? readServiceIdentity(dir) : null
|
|
712
766
|
proxyUrl = proxyUrl || saved?.proxy
|
|
713
767
|
masterPubkey = masterPubkey || saved?.iss
|
|
714
768
|
device = device || saved?.device
|
|
715
769
|
cert = cert || saved?.cert
|
|
716
|
-
if (typeof product !== 'string' || !product || typeof version !== 'string' || !version) throw new Error(
|
|
770
|
+
if (typeof product !== 'string' || !product || typeof version !== 'string' || !version) throw new Error(`${op}: needs product and version`)
|
|
717
771
|
if (!proxyUrl || !masterPubkey || !device || !cert) throw new Error('service not enrolled')
|
|
718
772
|
const client = await freshClient(proxyUrl, 20000, masterPubkey)
|
|
719
773
|
try {
|
|
720
774
|
await identifyAsService(client, device)
|
|
721
|
-
const data = { op
|
|
775
|
+
const data = { op, product, version, ...(typeof from === 'string' && from ? { from } : {}), publickey: device.publickey, ts: Date.now() }
|
|
722
776
|
const { signature } = await signWithDevice({ privateJwk: device.privateJwk, data })
|
|
723
|
-
const pending = waitForMsg(client, (p) => p.type === MSG.ERROR || (p.type === MSG.SECRETS_RESULT && p.body?.op ===
|
|
777
|
+
const pending = waitForMsg(client, (p) => p.type === MSG.ERROR || (p.type === MSG.SECRETS_RESULT && p.body?.op === op && p.body.product === product && p.body.version === version), timeoutMs)
|
|
724
778
|
client.sendByPubkey(masterPubkey, { type: MSG.SECRETS, data, signature, cert })
|
|
725
779
|
const res = await pending.catch((e) => {
|
|
726
|
-
if (/timeout/.test(e.message)) throw Object.assign(new Error(
|
|
780
|
+
if (/timeout/.test(e.message)) throw Object.assign(new Error(`${op}: the vault did not answer`), { code: 'vault-no-reply' })
|
|
727
781
|
throw e
|
|
728
782
|
})
|
|
729
783
|
if (res.type === MSG.ERROR) throw vaultError(res)
|
|
730
784
|
await verifyResponder({ body: res.body, seal: res.seal, acta: res.acta, masterPubkey, knownSeq: saved?.actaSeq ?? null })
|
|
731
|
-
if (res.body.ok !== true) throw new Error(
|
|
785
|
+
if (res.body.ok !== true) throw new Error(`malformed ${op} reply`)
|
|
732
786
|
return { ok: true }
|
|
733
787
|
} finally { client.close() }
|
|
734
788
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.148.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
|
@@ -28,7 +28,7 @@ import { watchBinary } from './selfupdate.js'
|
|
|
28
28
|
import { latestRelease, isNewer, CHECK_EVERY_MS, isUserInstall, installUserRelease } from './update.js'
|
|
29
29
|
import { VERSION } from './version.js'
|
|
30
30
|
import { readSettings } from './settings.js'
|
|
31
|
-
import { approversOfAll, firstYes, readAsked, writeAsked, clearAsked, shouldAsk, resultOfNo } from './updateApproval.js'
|
|
31
|
+
import { approversOfAll, firstYes, readAsked, writeAsked, clearAsked, shouldAsk, resultOfNo, readRootTold, writeRootTold, shouldTellRoot } from './updateApproval.js'
|
|
32
32
|
import { UPDATE_TTL_MS } from './approvals.js'
|
|
33
33
|
import crypto from 'node:crypto'
|
|
34
34
|
|
|
@@ -1161,6 +1161,25 @@ export async function runDaemon () {
|
|
|
1161
1161
|
// proceso no puede tocarlo. Se dice, con las dos salidas, y no se intenta nada.
|
|
1162
1162
|
if (!isUserInstall()) {
|
|
1163
1163
|
console.log(`[vault] version ${r.version} is out (this one is ${daemonVersion}) · installed as a system package, so it cannot update itself: \`dotrino-vault update\` (needs sudo), or move to a user install with the tarball's install.sh`)
|
|
1164
|
+
// …Y SE LE DICE A QUIEN APRUEBA, en todos los perfiles (dueño, 2026-10-08: «si se
|
|
1165
|
+
// requiere root por algún motivo, simplemente se notifica al aprobador que hay
|
|
1166
|
+
// actualización y necesita root»). El log no lo lee nadie; el teléfono sí. UNA vez por
|
|
1167
|
+
// versión, apuntado para que un reinicio no lo repita (`update-root-told.json`).
|
|
1168
|
+
let dicho
|
|
1169
|
+
try { dicho = readRootTold(dir) } catch (e) {
|
|
1170
|
+
console.log(`[vault] update: whether the approvers were already told could not be read (${e?.message || e}) · not telling them again`)
|
|
1171
|
+
return
|
|
1172
|
+
}
|
|
1173
|
+
if (shouldTellRoot(dicho, r.version, isNewer) && avisandoRoot !== r.version) {
|
|
1174
|
+
avisandoRoot = r.version
|
|
1175
|
+
contarATodos({ ev: 'update-needs-root', version: r.version, from: daemonVersion }, ({ todos, alguno }) => {
|
|
1176
|
+
avisandoRoot = null
|
|
1177
|
+
// Con que salió en algún perfil ya está dicho: repetirlo mañana a los que sí lo
|
|
1178
|
+
// oyeron por uno que no pudo sería timbrar de más.
|
|
1179
|
+
if (todos || alguno) { try { writeRootTold(dir, { version: r.version, at: Date.now() }) } catch (e) { console.log(`[vault] update: could not note that the approvers were told: ${e.message}`) } }
|
|
1180
|
+
else console.log(`[vault] update: the approvers could not be told that ${r.version} needs administrator rights · it will try again on the next check`)
|
|
1181
|
+
})
|
|
1182
|
+
}
|
|
1164
1183
|
return
|
|
1165
1184
|
}
|
|
1166
1185
|
if (actualizando) return
|
|
@@ -1245,6 +1264,7 @@ export async function runDaemon () {
|
|
|
1245
1264
|
}
|
|
1246
1265
|
let actualizando = false
|
|
1247
1266
|
let aprobada = null
|
|
1267
|
+
let avisandoRoot = null
|
|
1248
1268
|
|
|
1249
1269
|
/**
|
|
1250
1270
|
* «ME ACTUALICÉ»: se lo cuenta a quien aprueba la versión NUEVA, ya corriendo (dueño,
|
|
@@ -1263,6 +1283,18 @@ export async function runDaemon () {
|
|
|
1263
1283
|
if (isNewer(daemonVersion, m.to)) rm(updatedFile)
|
|
1264
1284
|
return // todavía no corre esa versión: se anuncia cuando arranque
|
|
1265
1285
|
}
|
|
1286
|
+
contarATodos({ version: m.to, from: typeof m.from === 'string' ? m.from : null }, ({ todos }) => {
|
|
1287
|
+
if (todos) rm(updatedFile)
|
|
1288
|
+
else console.log('[vault] update: some approvers were not told · it will try again on the next start')
|
|
1289
|
+
})
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* Cuenta algo a quien aprueba en TODOS los perfiles en marcha (`announceUpdate`), cada uno
|
|
1294
|
+
* en cuanto está identificado en el proxio: reintenta cada pocos segundos hasta que salió
|
|
1295
|
+
* en todos o se agotan los intentos. `alTerminar({ todos, alguno })`.
|
|
1296
|
+
*/
|
|
1297
|
+
function contarATodos (aviso, alTerminar) {
|
|
1266
1298
|
const hechos = new Set()
|
|
1267
1299
|
let intentos = 0
|
|
1268
1300
|
const intento = async () => {
|
|
@@ -1271,13 +1303,13 @@ export async function runDaemon () {
|
|
|
1271
1303
|
for (const p of perfilesEnMarcha()) {
|
|
1272
1304
|
if (hechos.has(p.id)) continue
|
|
1273
1305
|
if (!p.vault.isIdentified?.()) { falta = true; continue }
|
|
1274
|
-
try { await p.vault.announceUpdate(
|
|
1306
|
+
try { await p.vault.announceUpdate(aviso); hechos.add(p.id) } catch (e) {
|
|
1275
1307
|
falta = true
|
|
1276
1308
|
if (intentos === 1 || intentos === ANNOUNCE_TRIES) console.log(`[vault] update: profile ${p.name} could not tell its approvers yet: ${e?.message || e}`)
|
|
1277
1309
|
}
|
|
1278
1310
|
}
|
|
1279
|
-
if (!falta)
|
|
1280
|
-
if (intentos >= ANNOUNCE_TRIES)
|
|
1311
|
+
if (!falta) return alTerminar({ todos: true, alguno: hechos.size > 0 })
|
|
1312
|
+
if (intentos >= ANNOUNCE_TRIES) return alTerminar({ todos: false, alguno: hechos.size > 0 })
|
|
1281
1313
|
setTimeout(intento, ANNOUNCE_EVERY_MS).unref?.()
|
|
1282
1314
|
}
|
|
1283
1315
|
setTimeout(intento, ANNOUNCE_EVERY_MS).unref?.()
|
package/src/notices.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LO QUE LA BÓVEDA TIENE QUE CONTARLE A QUIEN APRUEBA, y que no es un pedido.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Son dos, de la bóveda o de uno de sus aparatos: «me actualicé» y «hay versión nueva y
|
|
5
|
+
* necesita permisos de administrador: instálala a mano» (`update-needs-root`). (`ev: 'updated'`, dueño 2026-10-08: «las actualizaciones, que se
|
|
5
6
|
* envíe una notificación de que ocurrieron a los aprobadores»). Sale como aviso por el
|
|
6
7
|
* proxio, pero el timbre de una app nativa llega VACÍO —lo leen Google y Apple—, así que la
|
|
7
8
|
* app pregunta por su lista (`op: 'approvals'`) y lo lee de aquí, en `notices`.
|
|
@@ -18,6 +19,8 @@ import crypto from 'node:crypto'
|
|
|
18
19
|
export const NOTICES_FILE = 'notices.json'
|
|
19
20
|
/** Cuánto se sigue contando un aviso. */
|
|
20
21
|
export const NOTICE_TTL_MS = 24 * 60 * 60 * 1000
|
|
22
|
+
/** Los avisos que existen. */
|
|
23
|
+
export const NOTICE_EVS = ['updated', 'update-needs-root']
|
|
21
24
|
|
|
22
25
|
const fail = (code, message) => Object.assign(new Error(message), { code })
|
|
23
26
|
|
|
@@ -47,14 +50,17 @@ export function openNotices ({ dir, atRest, now = Date.now }) {
|
|
|
47
50
|
|
|
48
51
|
return {
|
|
49
52
|
/**
|
|
50
|
-
* Apunta
|
|
51
|
-
*
|
|
53
|
+
* Apunta un aviso y lo devuelve tal como viaja. `ev`: `updated` (se actualizó) o
|
|
54
|
+
* `update-needs-root` (hay versión nueva y hay que instalarla a mano, con permisos de
|
|
55
|
+
* administrador). `product`: qué (`@dotrino/vaultd` es la propia bóveda);
|
|
56
|
+
* `deviceId`/`label`: el aparato, si fue uno.
|
|
52
57
|
*/
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
if (typeof
|
|
58
|
+
add (ev, { version, from = null, product = '@dotrino/vaultd', deviceId = null, label = null }) {
|
|
59
|
+
if (!NOTICE_EVS.includes(ev)) throw fail('bad-notice', `unknown notice: ${ev}`)
|
|
60
|
+
if (typeof version !== 'string' || !version) throw fail('bad-notice', `${ev}: a version is required`)
|
|
61
|
+
if (typeof product !== 'string' || !product) throw fail('bad-notice', `${ev}: a product is required`)
|
|
56
62
|
const n = {
|
|
57
|
-
id: crypto.randomBytes(8).toString('hex'), ev
|
|
63
|
+
id: crypto.randomBytes(8).toString('hex'), ev, product, version, from: typeof from === 'string' ? from : null,
|
|
58
64
|
...(typeof deviceId === 'string' && deviceId ? { deviceId, label: typeof label === 'string' ? label : '' } : {}),
|
|
59
65
|
ts: now()
|
|
60
66
|
}
|
|
@@ -62,6 +68,7 @@ export function openNotices ({ dir, atRest, now = Date.now }) {
|
|
|
62
68
|
save()
|
|
63
69
|
return n
|
|
64
70
|
},
|
|
71
|
+
updated (o) { return this.add('updated', o) },
|
|
65
72
|
/** Los avisos vivos, del más viejo al más nuevo. */
|
|
66
73
|
list () { return items.filter(alive).map((n) => ({ ...n })) }
|
|
67
74
|
}
|
package/src/updateApproval.js
CHANGED
|
@@ -108,3 +108,32 @@ export function shouldAsk (asked, version, isNewer) {
|
|
|
108
108
|
export function resultOfNo (askedAt, ttlMs, now = Date.now()) {
|
|
109
109
|
return now - askedAt >= ttlMs - 60_000 ? 'expired' : 'denied'
|
|
110
110
|
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* «NECESITO PERMISOS DE ADMINISTRADOR» SE DICE UNA VEZ POR VERSIÓN. Una bóveda instalada como
|
|
114
|
+
* paquete del sistema no puede cambiarse el binario: avisa a quien aprueba y espera a que
|
|
115
|
+
* alguien la instale a mano. Sin apuntarlo, lo repetiría en cada pasada y en cada reinicio.
|
|
116
|
+
*
|
|
117
|
+
* Mismas reglas que lo preguntado: sobrevive al reinicio, y un archivo que existe y no se
|
|
118
|
+
* puede leer se LANZA (ni «ya se dijo» ni «dilo otra vez»).
|
|
119
|
+
*/
|
|
120
|
+
export const ROOT_TOLD_FILE = 'update-root-told.json'
|
|
121
|
+
|
|
122
|
+
/** @returns {{ version: string, at: number } | null} */
|
|
123
|
+
export function readRootTold (dir) {
|
|
124
|
+
const file = path.join(dir, ROOT_TOLD_FILE)
|
|
125
|
+
if (!fs.existsSync(file)) return null
|
|
126
|
+
const d = ipcRead(file, null)
|
|
127
|
+
if (!d || typeof d.version !== 'string' || !d.version || typeof d.at !== 'number') throw fail('root-told-unreadable', `${ROOT_TOLD_FILE} exists but cannot be read`)
|
|
128
|
+
return { version: d.version, at: d.at }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function writeRootTold (dir, { version, at }) {
|
|
132
|
+
if (typeof version !== 'string' || !version || typeof at !== 'number') throw fail('bad-root-told', 'writeRootTold: version and at are required')
|
|
133
|
+
ipcWrite(path.join(dir, ROOT_TOLD_FILE), { v: 1, version, at })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** ¿Hay que avisar por `version`? Solo si nunca se avisó, o si es MÁS NUEVA que la avisada. */
|
|
137
|
+
export function shouldTellRoot (told, version, isNewer) {
|
|
138
|
+
return !told || isNewer(version, told.version)
|
|
139
|
+
}
|
package/src/vault.js
CHANGED
|
@@ -1617,31 +1617,36 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1617
1617
|
* aprueba en este perfil, por el mismo camino que su propio «me actualicé». Si el agente
|
|
1618
1618
|
* avisa o no lo decide él (su interruptor); aquí solo se reparte.
|
|
1619
1619
|
*
|
|
1620
|
+
* Y su hermano `op: 'update-needs-root'`: el aparato vio una versión nueva y no puede
|
|
1621
|
+
* instalarla solo porque necesita permisos de administrador. Mismo reparto, otro `ev`.
|
|
1622
|
+
*
|
|
1620
1623
|
* QUIÉN ES lo dice el acta (id y nombre), no lo que el aparato cuente de sí mismo.
|
|
1621
1624
|
*/
|
|
1622
1625
|
async function handleUpdateDone (from, p) {
|
|
1626
|
+
const op = p.data.op
|
|
1627
|
+
const ev = op === 'update-needs-root' ? 'update-needs-root' : 'updated'
|
|
1623
1628
|
const chk = await verifyChain({
|
|
1624
1629
|
data: p.data, signature: p.signature, cert: p.cert,
|
|
1625
1630
|
...(await contextoActa()), revoked: await revocationSet()
|
|
1626
1631
|
})
|
|
1627
|
-
if (!chk.ok) return denyChain(from, chk, p,
|
|
1632
|
+
if (!chk.ok) return denyChain(from, chk, p, op)
|
|
1628
1633
|
const record = (await identity.profileActa?.().catch(() => null))?.acta
|
|
1629
1634
|
const member = (record?.members || []).find((m) => m.pub === chk.device)
|
|
1630
1635
|
if (!member) {
|
|
1631
|
-
audit('rejected', { what:
|
|
1636
|
+
audit('rejected', { what: op, reason: record ? 'acta' : 'sin-acta' })
|
|
1632
1637
|
return reply(from, { type: MSG.ERROR, error: 'unauthorized: acta — this device is not in the record' })
|
|
1633
1638
|
}
|
|
1634
1639
|
const product = cut(p.data?.product, 128)
|
|
1635
1640
|
const version = cut(p.data?.version, 64)
|
|
1636
|
-
if (!product || !version) return reply(from, { type: MSG.ERROR, code: 'bad-request', error:
|
|
1641
|
+
if (!product || !version) return reply(from, { type: MSG.ERROR, code: 'bad-request', error: `${op}: needs \`product\` and \`version\`` })
|
|
1637
1642
|
const deviceId = await deviceIdOf(chk.device).catch(() => null)
|
|
1638
1643
|
try {
|
|
1639
|
-
await announceUpdate({ version, from: cut(p.data?.from, 64), product, deviceId, label: member.label || '' })
|
|
1644
|
+
await announceUpdate({ ev, version, from: cut(p.data?.from, 64), product, deviceId, label: member.label || '' })
|
|
1640
1645
|
} catch (e) {
|
|
1641
|
-
log(`[vault]
|
|
1642
|
-
return reply(from, { type: MSG.ERROR, code: e.code || 'announce-failed', error:
|
|
1646
|
+
log(`[vault] ${op}: ${deviceId || '????-????'} reported ${product} ${version}, but the approvers could not be told: ${e.message}`)
|
|
1647
|
+
return reply(from, { type: MSG.ERROR, code: e.code || 'announce-failed', error: `${op}: the approvers could not be told` })
|
|
1643
1648
|
}
|
|
1644
|
-
const body = { op
|
|
1649
|
+
const body = { op, ok: true, product, version, ts: Date.now() }
|
|
1645
1650
|
reply(from, { type: MSG.SECRETS_RESULT, body, seal: await sealOrFail(body), acta: record })
|
|
1646
1651
|
}
|
|
1647
1652
|
|
|
@@ -1713,7 +1718,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1713
1718
|
if (p.data?.op === 'store.recipients' || p.data?.op === 'store') return handleVarStore(from, p)
|
|
1714
1719
|
if (p.data?.op === 'digest') return handleDigest(from, p)
|
|
1715
1720
|
if (p.data?.op === 'update-ask') return handleUpdateAsk(from, p)
|
|
1716
|
-
if (p.data?.op === 'update-done') return handleUpdateDone(from, p)
|
|
1721
|
+
if (p.data?.op === 'update-done' || p.data?.op === 'update-needs-root') return handleUpdateDone(from, p)
|
|
1717
1722
|
if (['approvals', 'approve', 'deny', 'grants', 'grant-revoke', 'block', 'blocked'].includes(p.data?.op)) return handleApproval(from, p)
|
|
1718
1723
|
const ns = p.data?.ns
|
|
1719
1724
|
if (!isValidSecretsNs(ns)) return reply(from, { type: MSG.ERROR, error: 'secrets: invalid namespace' })
|
|
@@ -2083,18 +2088,22 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2083
2088
|
* También reparte el «me actualicé» de un APARATO (`op: 'update-done'`): entonces lleva su
|
|
2084
2089
|
* `product`, `deviceId` y `label`.
|
|
2085
2090
|
*
|
|
2091
|
+
* Y con `ev: 'update-needs-root'` cuenta lo contrario: hay versión nueva y NO se instaló
|
|
2092
|
+
* sola porque necesita permisos de administrador (dueño, 2026-10-08: «si se requiere root
|
|
2093
|
+
* por algún motivo, simplemente se notifica al aprobador»). Mismo camino, otro `ev`.
|
|
2094
|
+
*
|
|
2086
2095
|
* A diferencia de `notifyApprovers`, aquí el fallo SE LANZA: quien llama guarda el
|
|
2087
2096
|
* marcador hasta que el aviso salió, y tragarlo sería borrarlo sin haber avisado.
|
|
2088
2097
|
* @returns {Promise<{ approvers: number, sent: number, notice: object|null }>}
|
|
2089
2098
|
*/
|
|
2090
|
-
async function announceUpdate ({ version, from = null, product = '@dotrino/vaultd', deviceId = null, label = null }) {
|
|
2099
|
+
async function announceUpdate ({ version, from = null, product = '@dotrino/vaultd', deviceId = null, label = null, ev = 'updated' }) {
|
|
2091
2100
|
const record = await refreshActa()
|
|
2092
2101
|
const who = (record?.members || []).filter((m) => Acta.memberCan(record, m.pub, 'approve')).map((m) => m.pub)
|
|
2093
2102
|
if (!who.length) return { approvers: 0, sent: 0, notice: null }
|
|
2094
2103
|
// `deviceId`/`label`: solo cuando quien se actualizó es un APARATO (`update-done`). El
|
|
2095
2104
|
// aviso de la propia bóveda no los lleva.
|
|
2096
|
-
const notice = notices.
|
|
2097
|
-
const { id, ev, ts, ...rest } = notice
|
|
2105
|
+
const notice = notices.add(ev, { version, from, product, ...(deviceId ? { deviceId, label: label || '' } : {}) })
|
|
2106
|
+
const { id, ev: _ev, ts, ...rest } = notice
|
|
2098
2107
|
const body = { ev, id, ...rest, ts }
|
|
2099
2108
|
const seal = await sealOrFail(body)
|
|
2100
2109
|
let sent = 0
|
|
@@ -2102,8 +2111,8 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2102
2111
|
try { client.sendByPubkey(pub, { type: MSG.ADMIN_EVENT, body, seal }, { app: 'vault' }); sent++ }
|
|
2103
2112
|
catch (e) { log(`[vault] could not tell ${pub.slice(0, 24)}… about the update of ${product} to ${version}: ${e.message}`) }
|
|
2104
2113
|
}
|
|
2105
|
-
audit('update.announced', { product, version, from: notice.from, device: deviceId, approvers: who.length, sent })
|
|
2106
|
-
log(`[vault] update: told ${sent} of ${who.length} approver(s) that ${deviceId ? `${deviceId}
|
|
2114
|
+
audit(ev === 'updated' ? 'update.announced' : 'update.needs-root', { product, version, from: notice.from, device: deviceId, approvers: who.length, sent })
|
|
2115
|
+
log(`[vault] update: told ${sent} of ${who.length} approver(s) that ${deviceId ? `${deviceId} (${product})` : 'this vault'} ${ev === 'updated' ? 'now runs' : 'needs administrator rights to install'} ${version}`)
|
|
2107
2116
|
if (!sent) throw Object.assign(new Error('no approver could be told'), { code: 'announce-failed' })
|
|
2108
2117
|
return { approvers: who.length, sent, notice }
|
|
2109
2118
|
}
|