@dotrino/vaultd 0.129.1 → 0.131.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/package.json +1 -1
- package/src/ctl.js +28 -5
- package/src/daemon.js +60 -10
- package/src/secretsStore.js +18 -0
- package/src/tui/app.js +5 -0
- package/src/tui/i18n.js +4 -0
- package/src/update.js +57 -1
- package/src/vault.js +22 -5
- package/src/vaultControl.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.131.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/ctl.js
CHANGED
|
@@ -520,7 +520,9 @@ async function cmdMe () {
|
|
|
520
520
|
// El volcado es contenido del usuario: se lee y se BORRA, no se queda ahí suelto.
|
|
521
521
|
try { fs.rmSync(meFile, { force: true }) } catch (_) {}
|
|
522
522
|
if (!dump) { console.error('La bóveda no respondió. ¿Está corriendo? dotrino-vault status'); process.exit(1) }
|
|
523
|
-
|
|
523
|
+
// Con el perfil CERRADO se enseña igual lo público (la bóveda ya se lo da a cualquiera);
|
|
524
|
+
// lo que no sale son ni los nombres de los datos privados. Se dice.
|
|
525
|
+
if (dump.locked && !dump.me) assertOpen(dump)
|
|
524
526
|
|
|
525
527
|
const me = dump.me
|
|
526
528
|
if (!me) {
|
|
@@ -530,6 +532,7 @@ async function cmdMe () {
|
|
|
530
532
|
}
|
|
531
533
|
|
|
532
534
|
const when = me.updatedAt ? new Date(me.updatedAt).toLocaleString() : '—'
|
|
535
|
+
if (dump.locked) console.log('\n(perfil bloqueado: solo lo público; ábrelo con dotrino-vault unlock para ver qué datos privados hay)')
|
|
533
536
|
console.log('\n%sPerfil%s · actualizado %s\n', B, Z, when)
|
|
534
537
|
console.log(' nombre : %s', me.nickname || '(sin nombre)')
|
|
535
538
|
console.log(' foto : %s', me.avatar
|
|
@@ -546,6 +549,10 @@ async function cmdMe () {
|
|
|
546
549
|
console.log(' %s: %s%s', label.padEnd(12), me[k], me[k + 'Visible'] === false ? ' (oculto)' : '')
|
|
547
550
|
}
|
|
548
551
|
}
|
|
552
|
+
// Lo PRIVADO viaja sellado y la bóveda no lo abre para enseñarlo: se dice qué hay.
|
|
553
|
+
if (Array.isArray(me.privateKeys) && me.privateKeys.length) {
|
|
554
|
+
console.log('\n privados : %s (sellados: se ven en tus aparatos, no aquí)', me.privateKeys.join(', '))
|
|
555
|
+
}
|
|
549
556
|
for (const [title, list] of [['Enlaces', me.links], ['Otros datos', me.fields]]) {
|
|
550
557
|
if (!Array.isArray(list) || !list.length) continue
|
|
551
558
|
console.log('\n %s:', title)
|
|
@@ -1715,9 +1722,10 @@ async function cmdLogins (rest) {
|
|
|
1715
1722
|
/**
|
|
1716
1723
|
* `dotrino-vault update` — TRAER LA VERSIÓN NUEVA, cuando lo escribe una persona.
|
|
1717
1724
|
*
|
|
1718
|
-
*
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1725
|
+
* Con la INSTALACIÓN DE USUARIO (`~/.local/share/dotrino/bin`) el daemon ya se actualiza
|
|
1726
|
+
* solo: pide permiso al teléfono si hay aprobadores y, si no los hay, no hace falta
|
|
1727
|
+
* (dueño, 2026-09-24). Esto es lo mismo a mano, sin esperar al aviso del día. Con el
|
|
1728
|
+
* `.deb` del sistema el binario es de root y aquí solo se verifica y se dice el `sudo`.
|
|
1721
1729
|
*
|
|
1722
1730
|
* Lo que se baja SE VERIFICA contra la atestación de sigstore antes de tocar el disco, y
|
|
1723
1731
|
* si no se puede verificar NO SE INSTALA. Que la URL sea la correcta no prueba nada.
|
|
@@ -1726,7 +1734,7 @@ async function cmdLogins (rest) {
|
|
|
1726
1734
|
* va para que systemd lo levante con el nuevo (`src/selfupdate.js`).
|
|
1727
1735
|
*/
|
|
1728
1736
|
async function cmdUpdate (args = []) {
|
|
1729
|
-
const { latestRelease, assetFor, download, verifyArtifact, isNewer } = await import('./update.js')
|
|
1737
|
+
const { latestRelease, assetFor, download, verifyArtifact, isNewer, isUserInstall, installUserRelease } = await import('./update.js')
|
|
1730
1738
|
const soloMirar = args.includes('--check')
|
|
1731
1739
|
|
|
1732
1740
|
process.stdout.write('Mirando qué hay publicado… ')
|
|
@@ -1745,6 +1753,21 @@ async function cmdUpdate (args = []) {
|
|
|
1745
1753
|
console.log('%sHay %s y esta es la %s.%s', B, r.version, VERSION, Z)
|
|
1746
1754
|
if (soloMirar) { console.log('Tráela con: dotrino-vault update'); return }
|
|
1747
1755
|
|
|
1756
|
+
// Instalación de usuario: se verifica y se instala aquí mismo, sin sudo.
|
|
1757
|
+
if (isUserInstall()) {
|
|
1758
|
+
process.stdout.write('Bajando y comprobando la firma… ')
|
|
1759
|
+
const res = await installUserRelease(r)
|
|
1760
|
+
if (!res.ok) {
|
|
1761
|
+
console.log('')
|
|
1762
|
+
console.error('%sNO se instala nada.%s %s', R, Z, res.reason)
|
|
1763
|
+
if (res.file) console.error('El archivo quedó en %s, sin tocar nada.', res.file)
|
|
1764
|
+
process.exit(1)
|
|
1765
|
+
}
|
|
1766
|
+
console.log('firmada por el release de %s ✓', 'imdotrino/dotrino-vault')
|
|
1767
|
+
console.log('Instalada la %s. El servicio se reinicia solo; compruébalo con: dotrino-vault status', res.version)
|
|
1768
|
+
return
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1748
1771
|
const pick = assetFor(r.assets)
|
|
1749
1772
|
if (!pick.ok) { console.error('%s', pick.reason); process.exit(1) }
|
|
1750
1773
|
|
package/src/daemon.js
CHANGED
|
@@ -23,7 +23,7 @@ import { dataDir } from './paths.js'
|
|
|
23
23
|
import { ipcRead, ipcWrite, migrateIpcDir } from './ipc.js'
|
|
24
24
|
import { parseInvite } from '../lib/src/invite.js'
|
|
25
25
|
import { watchBinary } from './selfupdate.js'
|
|
26
|
-
import { latestRelease, isNewer, CHECK_EVERY_MS } from './update.js'
|
|
26
|
+
import { latestRelease, isNewer, CHECK_EVERY_MS, isUserInstall, installUserRelease } from './update.js'
|
|
27
27
|
import { VERSION } from './version.js'
|
|
28
28
|
import crypto from 'node:crypto'
|
|
29
29
|
|
|
@@ -342,6 +342,26 @@ export async function runDaemon () {
|
|
|
342
342
|
const meReqFile = path.join(dir, 'me-request.json')
|
|
343
343
|
const meFile = path.join(dir, 'me.json')
|
|
344
344
|
|
|
345
|
+
/**
|
|
346
|
+
* EL PERFIL PARA `dotrino-vault me` Y LA TUI, desde el cajón `@me` (donde escribe el aparato,
|
|
347
|
+
* dato a dato). Leía el `profileGet` del almacén viejo, al que ya no escribe nadie: un perfil
|
|
348
|
+
* nuevo salía SIEMPRE vacío aunque el aparato lo hubiera mandado (dueño, 2026-09-24).
|
|
349
|
+
*
|
|
350
|
+
* `conPrivados`: con el perfil CERRADO no se dicen ni los nombres de los datos privados —el
|
|
351
|
+
* candado esconde nombres igual que con las variables—. Lo público sí: la bóveda ya se lo
|
|
352
|
+
* da a cualquiera por el proxio (`profilePublic`), así que no revela nada.
|
|
353
|
+
*/
|
|
354
|
+
function perfilParaMostrar (vault, { conPrivados }) {
|
|
355
|
+
const v = vault.profileView()
|
|
356
|
+
const lista = (s) => { if (typeof s !== 'string') return undefined; try { const a = JSON.parse(s); return Array.isArray(a) ? a : undefined } catch (_) { return undefined } }
|
|
357
|
+
const { avatar, links, fields, ...rest } = v.public
|
|
358
|
+
if (!Object.keys(v.public).length && !(conPrivados && v.private.length)) return null
|
|
359
|
+
return {
|
|
360
|
+
...rest, avatar: avatarInfo(avatar), links: lista(links), fields: lista(fields),
|
|
361
|
+
...(conPrivados ? { privateKeys: v.private } : {}), updatedAt: v.updatedAt
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
345
365
|
/** Resumen de la foto de perfil: qué es y cuánto pesa, nunca los bytes. */
|
|
346
366
|
function avatarInfo (avatar) {
|
|
347
367
|
if (typeof avatar !== 'string' || !avatar) return null
|
|
@@ -366,7 +386,8 @@ export async function runDaemon () {
|
|
|
366
386
|
const v = mgr.get(id)
|
|
367
387
|
if (!v?.rekeySecrets) return
|
|
368
388
|
const r = await v.rekeySecrets(vieja, nueva)
|
|
369
|
-
|
|
389
|
+
// Se re-sella UNA copia (la de recuperación, que abre el llavero), no cajón por cajón.
|
|
390
|
+
if (r?.rekeyed) console.log('[vault] secrets recovery copy re-sealed under the new key')
|
|
370
391
|
}
|
|
371
392
|
|
|
372
393
|
/**
|
|
@@ -808,7 +829,11 @@ export async function runDaemon () {
|
|
|
808
829
|
ipcWrite(path.join(dir, 'acta.json'), { ...closed, members: [] })
|
|
809
830
|
}
|
|
810
831
|
rm(meReqFile)
|
|
811
|
-
if (meReq)
|
|
832
|
+
if (meReq) {
|
|
833
|
+
let me = null
|
|
834
|
+
try { me = perfilParaMostrar(t.vault, { conPrivados: false }) } catch (e) { console.error('[vault] could not dump the profile:', e.message) }
|
|
835
|
+
ipcWrite(meFile, { ...closed, req: meReq.id || null, me })
|
|
836
|
+
}
|
|
812
837
|
return
|
|
813
838
|
}
|
|
814
839
|
if (dumpReq) {
|
|
@@ -852,9 +877,8 @@ export async function runDaemon () {
|
|
|
852
877
|
rm(meReqFile)
|
|
853
878
|
try {
|
|
854
879
|
const tm = resolveTarget(meReq) || { id: mgr.currentId(), vault: mgr.current() }
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
ipcWrite(meFile, { v: 1, at: Date.now(), req: meReq.id || null, profile: tm.id, me: me ? { ...rest, avatar: avatarInfo(avatar) } : null })
|
|
880
|
+
const me = perfilParaMostrar(tm.vault, { conPrivados: true })
|
|
881
|
+
ipcWrite(meFile, { v: 1, at: Date.now(), req: meReq.id || null, profile: tm.id, me })
|
|
858
882
|
} catch (e) { console.error('[vault] could not dump the profile:', e.message) }
|
|
859
883
|
}
|
|
860
884
|
} catch (e) {
|
|
@@ -1025,11 +1049,37 @@ export async function runDaemon () {
|
|
|
1025
1049
|
*/
|
|
1026
1050
|
let quienAprueba = []
|
|
1027
1051
|
try { quienAprueba = (await mgr.current()?.approvers?.()) || [] } catch (_) {}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1052
|
+
|
|
1053
|
+
// INSTALADA COMO PAQUETE DEL SISTEMA (.deb en /usr/bin): el binario es de root y este
|
|
1054
|
+
// proceso no puede tocarlo. Se dice, con las dos salidas, y no se intenta nada.
|
|
1055
|
+
if (!isUserInstall()) {
|
|
1056
|
+
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`)
|
|
1057
|
+
return
|
|
1058
|
+
}
|
|
1059
|
+
if (actualizando) return
|
|
1060
|
+
actualizando = true
|
|
1061
|
+
try {
|
|
1062
|
+
// PIDE PERMISO SOLO SI HAY A QUIÉN PEDÍRSELO (dueño, 2026-09-20); sin aprobadores
|
|
1063
|
+
// no se necesita aprobación (dueño, 2026-09-24).
|
|
1064
|
+
if (quienAprueba.length) {
|
|
1065
|
+
console.log(`[vault] version ${r.version} is out (this one is ${daemonVersion}) · asking one of ${quienAprueba.length} approver(s)`)
|
|
1066
|
+
const si = await mgr.current().askUpdateApproval({ version: r.version })
|
|
1067
|
+
if (!si) { console.log(`[vault] update to ${r.version}: not approved (denied or expired) · it will ask again`); return }
|
|
1068
|
+
} else {
|
|
1069
|
+
console.log(`[vault] version ${r.version} is out (this one is ${daemonVersion}) · no approver in the record: updating on its own`)
|
|
1070
|
+
}
|
|
1071
|
+
const res = await installUserRelease(r, { log: (m) => console.log(m) })
|
|
1072
|
+
if (!res.ok) {
|
|
1073
|
+
// Lo que NO cuadra no se instala, y se dice por qué: el archivo queda a mano.
|
|
1074
|
+
console.log(`[vault] update to ${r.version} NOT installed: ${res.reason}${res.file ? ` (file left at ${res.file})` : ''}`)
|
|
1075
|
+
return
|
|
1076
|
+
}
|
|
1077
|
+
console.log(`[vault] update: ${r.version} installed · the service restarts on its own`)
|
|
1078
|
+
} catch (e) {
|
|
1079
|
+
console.log(`[vault] update to ${r.version} failed: ${e.message}`)
|
|
1080
|
+
} finally { actualizando = false }
|
|
1032
1081
|
}
|
|
1082
|
+
let actualizando = false
|
|
1033
1083
|
mirarRelease()
|
|
1034
1084
|
const relojRelease = setInterval(mirarRelease, CHECK_EVERY_MS)
|
|
1035
1085
|
relojRelease.unref?.()
|
package/src/secretsStore.js
CHANGED
|
@@ -909,6 +909,24 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
|
|
|
909
909
|
* Va firmado igual que lo demás: un dato público sin firma es un dato que cualquiera
|
|
910
910
|
* puede inventar en tu nombre, que es peor que no tenerlo.
|
|
911
911
|
*/
|
|
912
|
+
profileView () {
|
|
913
|
+
// EL PERFIL TAL COMO LO TIENE LA BÓVEDA, para enseñárselo al dueño (`dotrino-vault me`,
|
|
914
|
+
// la TUI). Sale del cajón `@me`, que es donde escribe el aparato dato a dato. Lo
|
|
915
|
+
// público va en claro; de lo privado solo el NOMBRE del dato: viaja sellado y la
|
|
916
|
+
// bóveda no lo abre para enseñarlo (§8.1), igual que una variable privada.
|
|
917
|
+
const vars = data.ns[PROFILE_NS]?.vars || {}
|
|
918
|
+
const pub = {}
|
|
919
|
+
const priv = []
|
|
920
|
+
let at = 0
|
|
921
|
+
for (const [key, e] of Object.entries(vars)) {
|
|
922
|
+
if (!e) continue
|
|
923
|
+
at = Math.max(at, e.at || 0)
|
|
924
|
+
if (e.cls === 'public' && typeof e.pubv === 'string') pub[key] = e.pubv
|
|
925
|
+
else priv.push(key)
|
|
926
|
+
}
|
|
927
|
+
return { public: pub, private: priv.sort(), updatedAt: at || null }
|
|
928
|
+
},
|
|
929
|
+
|
|
912
930
|
profilePublic () {
|
|
913
931
|
const bag = data.ns[PROFILE_NS]
|
|
914
932
|
if (!bag?.vars) return {}
|
package/src/tui/app.js
CHANGED
|
@@ -620,6 +620,11 @@ function meRows (st, t) {
|
|
|
620
620
|
if (filled.length) rows.push({ text: '', sel: false })
|
|
621
621
|
for (const [k, label] of filled) field(label, me[k], me[k + 'Visible'] === false)
|
|
622
622
|
|
|
623
|
+
// Lo PRIVADO viaja sellado y la bóveda no lo abre: se dice qué hay, no qué dice.
|
|
624
|
+
if (Array.isArray(me.privateKeys) && me.privateKeys.length) {
|
|
625
|
+
rows.push({ text: '', sel: false })
|
|
626
|
+
field(i.privateData, me.privateKeys.join(', ') + t.muted(i.privateDataHint))
|
|
627
|
+
}
|
|
623
628
|
for (const [title, list] of [[i.links, me.links], [i.otherData, me.fields]]) {
|
|
624
629
|
if (!Array.isArray(list) || !list.length) continue
|
|
625
630
|
rows.push({ text: '', sel: false })
|
package/src/tui/i18n.js
CHANGED
|
@@ -148,6 +148,8 @@ const es = {
|
|
|
148
148
|
fieldAddress: 'dirección',
|
|
149
149
|
links: 'Enlaces',
|
|
150
150
|
otherData: 'Otros datos',
|
|
151
|
+
privateData: 'Privados',
|
|
152
|
+
privateDataHint: ' (sellados: se ven en tus aparatos, no aquí)',
|
|
151
153
|
hidden: ' (oculto)',
|
|
152
154
|
no: 'no',
|
|
153
155
|
helpMe: ['←→ pestaña', 'F5 refrescar', 'Esc bóvedas', 'l English', 'q salir'],
|
|
@@ -537,6 +539,8 @@ const en = {
|
|
|
537
539
|
fieldAddress: 'address',
|
|
538
540
|
links: 'Links',
|
|
539
541
|
otherData: 'Other data',
|
|
542
|
+
privateData: 'Private',
|
|
543
|
+
privateDataHint: ' (sealed: they show on your devices, not here)',
|
|
540
544
|
hidden: ' (hidden)',
|
|
541
545
|
noName: '(no name)',
|
|
542
546
|
no: 'no',
|
package/src/update.js
CHANGED
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
* mismo son dos sitios donde arreglar el mismo fallo, y el segundo siempre se olvida.
|
|
11
11
|
*/
|
|
12
12
|
import fs from 'node:fs'
|
|
13
|
+
import os from 'node:os'
|
|
14
|
+
import path from 'node:path'
|
|
15
|
+
import { execFileSync } from 'node:child_process'
|
|
13
16
|
import { latestVersion, isNewer, CHECK_EVERY_MS } from '@dotrino/update'
|
|
14
|
-
import { pickAsset, download, verifyArtifact as verify } from '@dotrino/update/fetch'
|
|
17
|
+
import { pickAsset, download, verifyArtifact as verify, fetchVerified } from '@dotrino/update/fetch'
|
|
15
18
|
import { VERSION } from './version.js'
|
|
16
19
|
|
|
17
20
|
export const REPO = 'imdotrino/dotrino-vault'
|
|
@@ -37,3 +40,56 @@ export function assetFor (assets, { platform = process.platform, arch = process.
|
|
|
37
40
|
|
|
38
41
|
/** Verifica contra la atestación del release del vault. Sin `gh`, no se instala. */
|
|
39
42
|
export const verifyArtifact = (file, opts = {}) => verify(file, { repo: REPO, ...opts })
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* INSTALACIÓN DE USUARIO: los binarios viven en `~/.local/share/dotrino/bin`, que es del
|
|
47
|
+
* usuario, y la bóveda se actualiza sin `sudo` (dueño, 2026-09-24).
|
|
48
|
+
*
|
|
49
|
+
* Es esa carpeta y no `~/.local/bin` por una razón concreta: la unidad de systemd lleva
|
|
50
|
+
* `ProtectHome=read-only` y solo deja escribir en `~/.local/share/dotrino`. Desde ahí el
|
|
51
|
+
* daemon puede reemplazar su propio binario; en `~/.local/bin` no podría.
|
|
52
|
+
*
|
|
53
|
+
* El precio, dicho: un proceso de ese mismo usuario también podría reemplazarlo, cosa que
|
|
54
|
+
* con el `.deb` en `/usr/bin` (de root) no. En el modelo de la bóveda la frontera ya es el
|
|
55
|
+
* usuario (memoria `dotrino-aprobacion-comando`), y el binario nuevo solo entra verificado.
|
|
56
|
+
*/
|
|
57
|
+
export function userBinDir (home = os.homedir()) {
|
|
58
|
+
return path.join(home, '.local', 'share', 'dotrino', 'bin')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** ¿Corre este proceso desde la instalación de usuario? Solo entonces puede actualizarse solo. */
|
|
62
|
+
export function isUserInstall (execPath = process.execPath, home = os.homedir()) {
|
|
63
|
+
return path.dirname(execPath) === userBinDir(home)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Baja el tarball del release, lo VERIFICA contra su atestación y cambia los dos binarios
|
|
68
|
+
* de forma atómica (se escribe al lado y se renombra encima: nunca queda uno a medias).
|
|
69
|
+
* `selfupdate.js` ve el binario nuevo y el servicio se reinicia solo.
|
|
70
|
+
*
|
|
71
|
+
* Si la verificación falla no se toca nada, y se devuelve por qué.
|
|
72
|
+
*
|
|
73
|
+
* @returns {Promise<{ok:true, version:string} | {ok:false, code?:string, reason:string, file?:string}>}
|
|
74
|
+
*/
|
|
75
|
+
export async function installUserRelease (release, { binDir = userBinDir(), log = () => {}, fetchVerifiedImpl = fetchVerified } = {}) {
|
|
76
|
+
const pick = pickAsset(release.assets, [{ kind: 'tar', re: /-linux-x64\.tar\.gz$/ }])
|
|
77
|
+
if (!pick.ok) return pick
|
|
78
|
+
const got = await fetchVerifiedImpl(pick.asset, { repo: REPO, product: 'dotrino-vault', version: VERSION })
|
|
79
|
+
if (!got.ok) return got
|
|
80
|
+
log(`[vault] update: ${pick.asset.name} verified against its attestation`)
|
|
81
|
+
const work = path.dirname(got.file)
|
|
82
|
+
execFileSync('tar', ['xzf', got.file, '-C', work])
|
|
83
|
+
const src = fs.readdirSync(work).map((d) => path.join(work, d)).find((d) => fs.existsSync(path.join(d, 'dotrino-vaultd')))
|
|
84
|
+
if (!src) return { ok: false, code: 'BAD_TARBALL', reason: 'the tarball has no dotrino-vaultd inside' }
|
|
85
|
+
fs.mkdirSync(binDir, { recursive: true })
|
|
86
|
+
// El CLI primero y el daemon al final: cambiar el daemon es lo que dispara el reinicio.
|
|
87
|
+
for (const name of ['dotrino-vault', 'dotrino-vaultd']) {
|
|
88
|
+
const tmp = path.join(binDir, `.${name}.new`)
|
|
89
|
+
fs.copyFileSync(path.join(src, name), tmp)
|
|
90
|
+
fs.chmodSync(tmp, 0o755)
|
|
91
|
+
fs.renameSync(tmp, path.join(binDir, name))
|
|
92
|
+
}
|
|
93
|
+
try { fs.rmSync(work, { recursive: true, force: true }) } catch (_) {}
|
|
94
|
+
return { ok: true, version: release.version }
|
|
95
|
+
}
|
package/src/vault.js
CHANGED
|
@@ -37,7 +37,7 @@ import { loadManifest, brokenOf } from '@dotrino/roadmap'
|
|
|
37
37
|
// espera reventaba con un ReferenceError y la aprobación del mostrador de contraseñas no
|
|
38
38
|
// llegaba a existir. Solo se veía por ese camino —el único que lo usa—, y no había prueba
|
|
39
39
|
// que lo recorriera hasta que la hubo (dotrino-test, smoke:demonio, 2026-08-30).
|
|
40
|
-
import { createApprovals, createGrants, lastApproverGone, PENDING_TTL_MS, GRANT_TTL_MS } from './approvals.js'
|
|
40
|
+
import { createApprovals, createGrants, lastApproverGone, PENDING_TTL_MS, GRANT_TTL_MS, UPDATE_TTL_MS } from './approvals.js'
|
|
41
41
|
import { readProcContext, sanitizeContext, commandFingerprint, commandLine } from '../lib/src/proc.js'
|
|
42
42
|
import { makeContentKey, encryptWithCek, wrapForMember } from '@dotrino/identity/content'
|
|
43
43
|
import { makeSealer } from './sealer.js'
|
|
@@ -167,11 +167,11 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
167
167
|
* variables (la escritura se aplica al aprobar). Si nadie contesta, vence solo con un no:
|
|
168
168
|
* nada se queda colgado para siempre.
|
|
169
169
|
*/
|
|
170
|
-
async function awaitApproval ({ device, ns, kind, what, op = null, ctx = null, onAnswer }) {
|
|
170
|
+
async function awaitApproval ({ device, ns, kind, what, op = null, ctx = null, onAnswer, ttlMs = PENDING_TTL_MS }) {
|
|
171
171
|
const deviceId = await deviceIdOf(device).catch(() => null)
|
|
172
172
|
const record = await refreshActa()
|
|
173
173
|
const label = (record?.members || []).find((m) => m.pub === device)?.label || ''
|
|
174
|
-
const pend = approvals.request({ ns, device, deviceId, label, ek: '', ctx, kind })
|
|
174
|
+
const pend = approvals.request({ ns, device, deviceId, label, ek: '', ctx, kind, ttlMs })
|
|
175
175
|
audit(`${what}.pending`, { device: deviceId, ns, id: pend.id, op })
|
|
176
176
|
log(`[vault] ${what}: ${deviceId || '????-????'} is waiting for approval (${pend.id})`)
|
|
177
177
|
waiters.set(pend.id, onAnswer)
|
|
@@ -180,7 +180,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
180
180
|
audit(`${what}.expired`, { device: deviceId, ns, id: pend.id })
|
|
181
181
|
onAnswer(false)
|
|
182
182
|
}
|
|
183
|
-
},
|
|
183
|
+
}, ttlMs)
|
|
184
184
|
t.unref?.()
|
|
185
185
|
await notifyApprovers(pend, record)
|
|
186
186
|
return pend
|
|
@@ -1621,7 +1621,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1621
1621
|
const resolver = waiters.get(id)
|
|
1622
1622
|
waiters.delete(id)
|
|
1623
1623
|
const ok = op === 'approve'
|
|
1624
|
-
const what = pend.kind === 'write' ? 'vars' : 'passwords'
|
|
1624
|
+
const what = pend.kind === 'write' ? 'vars' : pend.kind === 'update' ? 'update' : 'passwords'
|
|
1625
1625
|
audit(ok ? `${what}.approved` : `${what}.denied`, { device: pend.deviceId, ns: pend.ns, id, by })
|
|
1626
1626
|
log(`[vault] ${what}: request ${pend.id} of ${pend.deviceId} ${ok ? 'approved' : 'DENIED'} by ${by}`)
|
|
1627
1627
|
// Lo aprobado SE HACE AQUÍ (una escritura se aplica ahora), y si falla quien aprobó
|
|
@@ -3655,6 +3655,21 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
3655
3655
|
* aprobadores que se quedaría sin poder actualizarse nunca, esperando un sí que nadie
|
|
3656
3656
|
* puede dar. Mismo sitio del que ya sale el «rang N approver(s)» de los secretos.
|
|
3657
3657
|
*/
|
|
3658
|
+
/**
|
|
3659
|
+
* PEDIR PERMISO PARA ACTUALIZARSE a `version` (la instala el daemon si se aprueba). Es
|
|
3660
|
+
* un pedido como los demás —sale en Pedidos del teléfono, `kind: 'update'`—, pero dura
|
|
3661
|
+
* un día (`UPDATE_TTL_MS`): no hay nadie esperando delante de una terminal.
|
|
3662
|
+
* @returns {Promise<boolean>} sí o no; vencido es no.
|
|
3663
|
+
*/
|
|
3664
|
+
askUpdateApproval: async ({ version }) => {
|
|
3665
|
+
const yo = identity.me?.publickey || null
|
|
3666
|
+
return new Promise((resolve) => {
|
|
3667
|
+
awaitApproval({
|
|
3668
|
+
device: yo, ns: 'vault', kind: 'update', what: 'update',
|
|
3669
|
+
ctx: { version, from: VERSION }, ttlMs: UPDATE_TTL_MS, onAnswer: resolve
|
|
3670
|
+
}).catch((e) => { log(`[vault] update: could not ask for approval: ${e.message}`); resolve(false) })
|
|
3671
|
+
})
|
|
3672
|
+
},
|
|
3658
3673
|
approvers: async () => {
|
|
3659
3674
|
const record = await refreshActa()
|
|
3660
3675
|
if (!record) return []
|
|
@@ -3703,6 +3718,8 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
3703
3718
|
return { ok: true, pub: found.pub, deviceId: found.deviceId }
|
|
3704
3719
|
},
|
|
3705
3720
|
listApprovals: () => approvals.list(),
|
|
3721
|
+
// El perfil desde el cajón `@me` (lo que empujan los aparatos): ver `profileView`.
|
|
3722
|
+
profileView: () => secrets.profileView(),
|
|
3706
3723
|
// LAS CONCESIONES VIVAS, para verlas y para cortarlas. Se enseñan en la máquina del
|
|
3707
3724
|
// dueño (TUI/CLI), donde el contexto ya es suyo y no hay nada que sellar.
|
|
3708
3725
|
listGrants: () => grants.list(),
|
package/src/vaultControl.js
CHANGED
|
@@ -500,7 +500,8 @@ export async function getMe (profile) {
|
|
|
500
500
|
const d = await waitFor(F.me, { req: id, since })
|
|
501
501
|
rm(F.me)
|
|
502
502
|
if (!d) throw coded('the daemon did not reply', 'NO_REPLY')
|
|
503
|
-
|
|
503
|
+
// Con el perfil cerrado llega igual lo público (ver `perfilParaMostrar` en daemon.js).
|
|
504
|
+
if (d.locked && !d.me) assertOpen(d)
|
|
504
505
|
return d.me || null
|
|
505
506
|
}
|
|
506
507
|
|