@dotrino/vaultd 0.129.0 → 0.130.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 +2 -2
- package/src/ctl.js +21 -5
- package/src/daemon.js +31 -5
- package/src/update.js +57 -1
- package/src/vault.js +20 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.130.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": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@dotrino/proxy-client": "0.24.0",
|
|
29
29
|
"@dotrino/roadmap": "0.3.7",
|
|
30
30
|
"@dotrino/store": "0.11.0",
|
|
31
|
-
"@dotrino/update": "0.
|
|
31
|
+
"@dotrino/update": "0.2.1",
|
|
32
32
|
"ws": "^8.18.0"
|
|
33
33
|
},
|
|
34
34
|
"license": "MIT",
|
package/src/ctl.js
CHANGED
|
@@ -1715,9 +1715,10 @@ async function cmdLogins (rest) {
|
|
|
1715
1715
|
/**
|
|
1716
1716
|
* `dotrino-vault update` — TRAER LA VERSIÓN NUEVA, cuando lo escribe una persona.
|
|
1717
1717
|
*
|
|
1718
|
-
*
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1718
|
+
* Con la INSTALACIÓN DE USUARIO (`~/.local/share/dotrino/bin`) el daemon ya se actualiza
|
|
1719
|
+
* solo: pide permiso al teléfono si hay aprobadores y, si no los hay, no hace falta
|
|
1720
|
+
* (dueño, 2026-09-24). Esto es lo mismo a mano, sin esperar al aviso del día. Con el
|
|
1721
|
+
* `.deb` del sistema el binario es de root y aquí solo se verifica y se dice el `sudo`.
|
|
1721
1722
|
*
|
|
1722
1723
|
* Lo que se baja SE VERIFICA contra la atestación de sigstore antes de tocar el disco, y
|
|
1723
1724
|
* si no se puede verificar NO SE INSTALA. Que la URL sea la correcta no prueba nada.
|
|
@@ -1726,7 +1727,7 @@ async function cmdLogins (rest) {
|
|
|
1726
1727
|
* va para que systemd lo levante con el nuevo (`src/selfupdate.js`).
|
|
1727
1728
|
*/
|
|
1728
1729
|
async function cmdUpdate (args = []) {
|
|
1729
|
-
const { latestRelease, assetFor, download, verifyArtifact, isNewer } = await import('./update.js')
|
|
1730
|
+
const { latestRelease, assetFor, download, verifyArtifact, isNewer, isUserInstall, installUserRelease } = await import('./update.js')
|
|
1730
1731
|
const soloMirar = args.includes('--check')
|
|
1731
1732
|
|
|
1732
1733
|
process.stdout.write('Mirando qué hay publicado… ')
|
|
@@ -1745,6 +1746,21 @@ async function cmdUpdate (args = []) {
|
|
|
1745
1746
|
console.log('%sHay %s y esta es la %s.%s', B, r.version, VERSION, Z)
|
|
1746
1747
|
if (soloMirar) { console.log('Tráela con: dotrino-vault update'); return }
|
|
1747
1748
|
|
|
1749
|
+
// Instalación de usuario: se verifica y se instala aquí mismo, sin sudo.
|
|
1750
|
+
if (isUserInstall()) {
|
|
1751
|
+
process.stdout.write('Bajando y comprobando la firma… ')
|
|
1752
|
+
const res = await installUserRelease(r)
|
|
1753
|
+
if (!res.ok) {
|
|
1754
|
+
console.log('')
|
|
1755
|
+
console.error('%sNO se instala nada.%s %s', R, Z, res.reason)
|
|
1756
|
+
if (res.file) console.error('El archivo quedó en %s, sin tocar nada.', res.file)
|
|
1757
|
+
process.exit(1)
|
|
1758
|
+
}
|
|
1759
|
+
console.log('firmada por el release de %s ✓', 'imdotrino/dotrino-vault')
|
|
1760
|
+
console.log('Instalada la %s. El servicio se reinicia solo; compruébalo con: dotrino-vault status', res.version)
|
|
1761
|
+
return
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1748
1764
|
const pick = assetFor(r.assets)
|
|
1749
1765
|
if (!pick.ok) { console.error('%s', pick.reason); process.exit(1) }
|
|
1750
1766
|
|
|
@@ -1754,7 +1770,7 @@ async function cmdUpdate (args = []) {
|
|
|
1754
1770
|
console.log('listo')
|
|
1755
1771
|
|
|
1756
1772
|
process.stdout.write('Comprobando la firma… ')
|
|
1757
|
-
const v = verifyArtifact(file)
|
|
1773
|
+
const v = await verifyArtifact(file)
|
|
1758
1774
|
if (!v.ok) {
|
|
1759
1775
|
console.log('')
|
|
1760
1776
|
console.error('%sNO se instala nada.%s %s', R, Z, v.reason)
|
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
|
|
|
@@ -1025,11 +1025,37 @@ export async function runDaemon () {
|
|
|
1025
1025
|
*/
|
|
1026
1026
|
let quienAprueba = []
|
|
1027
1027
|
try { quienAprueba = (await mgr.current()?.approvers?.()) || [] } catch (_) {}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1028
|
+
|
|
1029
|
+
// INSTALADA COMO PAQUETE DEL SISTEMA (.deb en /usr/bin): el binario es de root y este
|
|
1030
|
+
// proceso no puede tocarlo. Se dice, con las dos salidas, y no se intenta nada.
|
|
1031
|
+
if (!isUserInstall()) {
|
|
1032
|
+
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`)
|
|
1033
|
+
return
|
|
1034
|
+
}
|
|
1035
|
+
if (actualizando) return
|
|
1036
|
+
actualizando = true
|
|
1037
|
+
try {
|
|
1038
|
+
// PIDE PERMISO SOLO SI HAY A QUIÉN PEDÍRSELO (dueño, 2026-09-20); sin aprobadores
|
|
1039
|
+
// no se necesita aprobación (dueño, 2026-09-24).
|
|
1040
|
+
if (quienAprueba.length) {
|
|
1041
|
+
console.log(`[vault] version ${r.version} is out (this one is ${daemonVersion}) · asking one of ${quienAprueba.length} approver(s)`)
|
|
1042
|
+
const si = await mgr.current().askUpdateApproval({ version: r.version })
|
|
1043
|
+
if (!si) { console.log(`[vault] update to ${r.version}: not approved (denied or expired) · it will ask again`); return }
|
|
1044
|
+
} else {
|
|
1045
|
+
console.log(`[vault] version ${r.version} is out (this one is ${daemonVersion}) · no approver in the record: updating on its own`)
|
|
1046
|
+
}
|
|
1047
|
+
const res = await installUserRelease(r, { log: (m) => console.log(m) })
|
|
1048
|
+
if (!res.ok) {
|
|
1049
|
+
// Lo que NO cuadra no se instala, y se dice por qué: el archivo queda a mano.
|
|
1050
|
+
console.log(`[vault] update to ${r.version} NOT installed: ${res.reason}${res.file ? ` (file left at ${res.file})` : ''}`)
|
|
1051
|
+
return
|
|
1052
|
+
}
|
|
1053
|
+
console.log(`[vault] update: ${r.version} installed · the service restarts on its own`)
|
|
1054
|
+
} catch (e) {
|
|
1055
|
+
console.log(`[vault] update to ${r.version} failed: ${e.message}`)
|
|
1056
|
+
} finally { actualizando = false }
|
|
1032
1057
|
}
|
|
1058
|
+
let actualizando = false
|
|
1033
1059
|
mirarRelease()
|
|
1034
1060
|
const relojRelease = setInterval(mirarRelease, CHECK_EVERY_MS)
|
|
1035
1061
|
relojRelease.unref?.()
|
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 []
|