@dotrino/vaultd 0.157.0 → 0.158.1
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/README.md +14 -9
- package/bin/dotrino-vault-tui.js +1 -1
- package/bin/sea-entry.js +3 -3
- package/lib/src/atrest.js +2 -2
- package/lib/src/core/libVersion.js +1 -1
- package/package.json +3 -2
- package/src/ctl.js +33 -1
- package/src/daemon.js +21 -6
- package/src/service.js +10 -6
- package/src/tui/app.js +7 -3
- package/src/tui/i18n.js +2 -0
- package/src/update.js +141 -1
package/README.md
CHANGED
|
@@ -159,12 +159,18 @@ con `dotrino-vault service autostart on|off`. Desinstalar no borra tus datos.
|
|
|
159
159
|
Aplicaciones. Dentro van la ventana y la bóveda. Para que arranque al iniciar sesión, en
|
|
160
160
|
Ajustes (un LaunchAgent de tu usuario, `~/Library/LaunchAgents/com.dotrino.vault.plist`).
|
|
161
161
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
**Se actualizan solas, como en Linux** (CONVENCIONES §15): una vez al día miran si hay versión
|
|
163
|
+
nueva, bajan el instalador (Windows) o la app (macOS), lo **verifican contra la atestación de
|
|
164
|
+
sigstore** del release —dentro del propio binario, sin `gh`— y lo instalan. En Windows el
|
|
165
|
+
instalador cierra la bóveda, la cambia y la vuelve a arrancar (y vuelve a abrir la ventana si
|
|
166
|
+
estaba abierta); en macOS se cambia la app entera y la levanta launchd o, suelta, un
|
|
167
|
+
relanzador. La aprobación opcional (`update --approval on`) vale igual.
|
|
168
|
+
|
|
169
|
+
> El instalador de Windows **no lleva firma de Microsoft, y no la va a llevar** (decisión del
|
|
170
|
+
> dueño): Windows enseña el aviso de SmartScreen («editor desconocido»). La app de macOS
|
|
171
|
+
> todavía no lleva la firma de Apple: Gatekeeper avisa la primera vez (clic derecho → Abrir).
|
|
172
|
+
> Lo que prueba de dónde salieron es la atestación de sigstore:
|
|
173
|
+
> `gh attestation verify <archivo> --repo imdotrino/dotrino-vault`.
|
|
168
174
|
|
|
169
175
|
**Ubuntu / Debian — `.deb`:** descarga el `.deb` (versionado) desde
|
|
170
176
|
[Releases](https://github.com/imdotrino/dotrino-vault/releases/latest) y haz doble
|
|
@@ -893,9 +899,8 @@ ed25519 en formato OpenSSH (sin frase: la bóveda es el candado) y RSA/P-256 en
|
|
|
893
899
|
actividad. Contraseña opcional por perfil, **para editarlo** (verificador PBKDF2).
|
|
894
900
|
- **v2:** cifrado en reposo **con la contraseña del perfil** (hoy la clave sale solo de
|
|
895
901
|
la máquina) y atado a tu cuenta del sistema —DPAPI en Windows, Keychain en macOS— o
|
|
896
|
-
al TPM; cifrar también los secretos y el store;
|
|
897
|
-
|
|
898
|
-
con sellado de tiempo (`dotrino-signer`).
|
|
902
|
+
al TPM; cifrar también los secretos y el store; firma y notarización de Apple para la app de
|
|
903
|
+
macOS; firma de documentos con sellado de tiempo (`dotrino-signer`).
|
|
899
904
|
|
|
900
905
|
## Estructura
|
|
901
906
|
|
package/bin/dotrino-vault-tui.js
CHANGED
|
@@ -23,6 +23,6 @@ if (!process.stdout.isTTY) {
|
|
|
23
23
|
|
|
24
24
|
runTui().catch((e) => {
|
|
25
25
|
// El finally de runTui ya restauró el terminal; aquí solo reportamos.
|
|
26
|
-
console.error('error
|
|
26
|
+
console.error('TUI error:', e?.stack || e?.message || e)
|
|
27
27
|
process.exit(1)
|
|
28
28
|
})
|
package/bin/sea-entry.js
CHANGED
|
@@ -16,11 +16,11 @@ if (argv[0] === '--tui') {
|
|
|
16
16
|
// ni el proxy: habla con el daemon por el dir de datos + señales.
|
|
17
17
|
if (!process.stdout.isTTY) { console.error('la TUI necesita un terminal interactivo (TTY).'); process.exit(2) }
|
|
18
18
|
import('../src/tui/app.js').then(({ runTui }) => runTui())
|
|
19
|
-
.catch((e) => { console.error('error
|
|
19
|
+
.catch((e) => { console.error('TUI error:', e.message); process.exit(1) })
|
|
20
20
|
} else if (argv[0] === '--gui-bridge') {
|
|
21
21
|
// El canal de la ventana de escritorio (JSON por stdin/stdout, ver src/tui/gui.js).
|
|
22
22
|
import('../src/tui/gui.js').then(({ runGuiBridge }) => runGuiBridge()).then(() => process.exit(0))
|
|
23
|
-
.catch((e) => { console.error('
|
|
23
|
+
.catch((e) => { console.error('window bridge error:', e.message); process.exit(1) })
|
|
24
24
|
} else if (argv[0] === '--ctl') {
|
|
25
25
|
// CLI de control: NO toca la identidad ni el proxy; habla con el daemon vía
|
|
26
26
|
// el dir de datos (state.json) y señales (SIGUSR1 para emparejar).
|
|
@@ -29,5 +29,5 @@ if (argv[0] === '--tui') {
|
|
|
29
29
|
} else {
|
|
30
30
|
// Modo daemon (servicio).
|
|
31
31
|
import('../src/daemon.js').then(({ runDaemon }) => runDaemon(argv))
|
|
32
|
-
.catch((e) => { console.error('
|
|
32
|
+
.catch((e) => { console.error('fatal daemon error:', e.message); process.exit(1) })
|
|
33
33
|
}
|
package/lib/src/atrest.js
CHANGED
|
@@ -132,7 +132,7 @@ export function encryptText (plaintext, key) {
|
|
|
132
132
|
|
|
133
133
|
export function decryptText (blob, key) {
|
|
134
134
|
const [magic, iv, ct, tag] = String(blob).split('.')
|
|
135
|
-
if (magic !== MAGIC) throw new Error('at-rest:
|
|
135
|
+
if (magic !== MAGIC) throw new Error('at-rest: unknown format')
|
|
136
136
|
const d = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'base64'))
|
|
137
137
|
d.setAuthTag(Buffer.from(tag, 'base64'))
|
|
138
138
|
return Buffer.concat([d.update(Buffer.from(ct, 'base64')), d.final()]).toString('utf8')
|
|
@@ -176,7 +176,7 @@ export function migrateFile (file, key) {
|
|
|
176
176
|
try { text = fs.readFileSync(file, 'utf8') } catch (_) { return 'sin-archivo' }
|
|
177
177
|
if (isEncrypted(text)) return 'ya-cifrado'
|
|
178
178
|
const blob = encryptText(text, key)
|
|
179
|
-
if (decryptText(blob, key) !== text) throw new Error('at-rest:
|
|
179
|
+
if (decryptText(blob, key) !== text) throw new Error('at-rest: verification failed, the original is untouched')
|
|
180
180
|
const tmp = file + '.atrest.tmp'
|
|
181
181
|
fs.writeFileSync(tmp, blob, { mode: 0o600 })
|
|
182
182
|
fs.renameSync(tmp, file)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.158.1",
|
|
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,8 @@
|
|
|
28
28
|
"@dotrino/proxy-client": "^0.32.2",
|
|
29
29
|
"@dotrino/roadmap": "0.4.13",
|
|
30
30
|
"@dotrino/store": "0.11.0",
|
|
31
|
-
"@dotrino/update": "0.
|
|
31
|
+
"@dotrino/update": "0.9.0",
|
|
32
|
+
"sigstore": "5.0.0",
|
|
32
33
|
"ws": "^8.18.0"
|
|
33
34
|
},
|
|
34
35
|
"license": "MIT",
|
package/src/ctl.js
CHANGED
|
@@ -1855,7 +1855,7 @@ async function cmdUpdate (args = []) {
|
|
|
1855
1855
|
: 'Esta bóveda se actualiza sola, sin pedir aprobación.')
|
|
1856
1856
|
return
|
|
1857
1857
|
}
|
|
1858
|
-
const { latestRelease, assetFor, download, verifyArtifact, isNewer, isUserInstall, installUserRelease } = await import('./update.js')
|
|
1858
|
+
const { latestRelease, assetFor, download, verifyArtifact, isNewer, isUserInstall, installUserRelease, installKind, installRelease } = await import('./update.js')
|
|
1859
1859
|
const soloMirar = args.includes('--check')
|
|
1860
1860
|
|
|
1861
1861
|
process.stdout.write('Mirando qué hay publicado… ')
|
|
@@ -1874,6 +1874,38 @@ async function cmdUpdate (args = []) {
|
|
|
1874
1874
|
console.log('%sHay %s y esta es la %s.%s', B, r.version, VERSION, Z)
|
|
1875
1875
|
if (soloMirar) { console.log('Tráela con: dotrino-vault update'); return }
|
|
1876
1876
|
|
|
1877
|
+
// WINDOWS Y macOS: el instalador nuevo (Windows) o la app entera (macOS), verificados.
|
|
1878
|
+
const kind = installKind()
|
|
1879
|
+
if (kind === 'inno' || kind === 'app') {
|
|
1880
|
+
process.stdout.write('Bajando y comprobando la firma… ')
|
|
1881
|
+
const res = await installRelease(r, { kind })
|
|
1882
|
+
if (!res.ok) {
|
|
1883
|
+
console.log('')
|
|
1884
|
+
console.error('%sNO se instala nada.%s %s', R, Z, res.reason)
|
|
1885
|
+
if (res.file) console.error('El archivo quedó en %s, sin tocar nada.', res.file)
|
|
1886
|
+
process.exit(1)
|
|
1887
|
+
}
|
|
1888
|
+
console.log('firmada por el release de %s ✓', 'imdotrino/dotrino-vault')
|
|
1889
|
+
await noteUpdated(res.version)
|
|
1890
|
+
if (kind === 'inno') {
|
|
1891
|
+
// El instalador cierra la bóveda (y este mismo comando: es el mismo .exe), la cambia y
|
|
1892
|
+
// la vuelve a arrancar. Aquí no queda nada que esperar.
|
|
1893
|
+
console.log('Instalando la %s: la bóveda se cierra un momento y vuelve sola.', res.version)
|
|
1894
|
+
return
|
|
1895
|
+
}
|
|
1896
|
+
// macOS: la app nueva ya está en su sitio; falta reiniciar la que corre.
|
|
1897
|
+
const s = ipcRead(stateFile, null)
|
|
1898
|
+
if (s && alive(s.pid)) {
|
|
1899
|
+
const { restartDaemonService } = await import('./service.js')
|
|
1900
|
+
try { await restartDaemonService({ pid: s.pid, alive: () => alive(s.pid) }) } catch (e) {
|
|
1901
|
+
console.error('Instalada, pero no se pudo reiniciar: %s', e.message)
|
|
1902
|
+
process.exit(1)
|
|
1903
|
+
}
|
|
1904
|
+
await waitForService(res.version)
|
|
1905
|
+
} else console.log('Instalada la %s.', res.version)
|
|
1906
|
+
return
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1877
1909
|
// Instalación de usuario: se verifica y se instala aquí mismo, sin sudo.
|
|
1878
1910
|
if (isUserInstall()) {
|
|
1879
1911
|
process.stdout.write('Bajando y comprobando la firma… ')
|
package/src/daemon.js
CHANGED
|
@@ -25,7 +25,7 @@ import { parseInvite } from '../lib/src/invite.js'
|
|
|
25
25
|
import { scopeToCaps, scopeToCn } from '../lib/src/enroll.js'
|
|
26
26
|
import { capScope } from '@dotrino/identity/acta'
|
|
27
27
|
import { watchBinary } from './selfupdate.js'
|
|
28
|
-
import { latestRelease, isNewer, CHECK_EVERY_MS,
|
|
28
|
+
import { latestRelease, isNewer, CHECK_EVERY_MS, installKind, installRelease } from './update.js'
|
|
29
29
|
import { VERSION } from './version.js'
|
|
30
30
|
import { readSettings } from './settings.js'
|
|
31
31
|
import { approversOfAll, firstYes, readAsked, writeAsked, clearAsked, shouldAsk, resultOfNo, readRootTold, writeRootTold, shouldTellRoot } from './updateApproval.js'
|
|
@@ -1157,9 +1157,18 @@ export async function runDaemon () {
|
|
|
1157
1157
|
}
|
|
1158
1158
|
const conAvisos = quienAprueba.filter((a) => a.notifiedAt).length
|
|
1159
1159
|
|
|
1160
|
-
//
|
|
1161
|
-
//
|
|
1162
|
-
|
|
1160
|
+
// CÓMO SE INSTALÓ decide si puede instalar ella misma (update.js, `installKind`): la de
|
|
1161
|
+
// usuario de Linux, el instalador de Windows y la app de macOS, sí. Por npm, Docker o un
|
|
1162
|
+
// checkout se actualiza por su propia vía: se dice en el log y nada más (no es «hace
|
|
1163
|
+
// falta root», y avisar eso al teléfono sería mentirle).
|
|
1164
|
+
const kind = installKind()
|
|
1165
|
+
if (kind === 'other') {
|
|
1166
|
+
if (avisadoOtra !== r.version) { avisadoOtra = r.version; console.log(`[vault] version ${r.version} is out (this one is ${daemonVersion}) · update it the way you installed it (npm, Docker, git)`) }
|
|
1167
|
+
return
|
|
1168
|
+
}
|
|
1169
|
+
// INSTALADA COMO PAQUETE DEL SISTEMA (.deb o .rpm en /usr/bin, o una app en una carpeta
|
|
1170
|
+
// que no es tuya): este proceso no puede tocarla. Se dice, con las salidas, y no se intenta nada.
|
|
1171
|
+
if (kind === 'system') {
|
|
1163
1172
|
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
1173
|
// …Y SE LE DICE A QUIEN APRUEBA, en todos los perfiles (dueño, 2026-10-08: «si se
|
|
1165
1174
|
// requiere root por algún motivo, simplemente se notifica al aprobador que hay
|
|
@@ -1235,7 +1244,7 @@ export async function runDaemon () {
|
|
|
1235
1244
|
}
|
|
1236
1245
|
clearAsked(dir)
|
|
1237
1246
|
if (pedidoSinRespuesta) { pedidoSinRespuesta = null; writeState() }
|
|
1238
|
-
const res = await
|
|
1247
|
+
const res = await installRelease(r, { log: (m) => console.log(m), kind, pid: process.pid })
|
|
1239
1248
|
if (!res.ok) {
|
|
1240
1249
|
// Lo que NO cuadra no se instala, y se dice por qué: el archivo queda a mano.
|
|
1241
1250
|
console.log(`[vault] update to ${r.version} NOT installed: ${res.reason}${res.file ? ` (file left at ${res.file})` : ''}`)
|
|
@@ -1252,7 +1261,12 @@ export async function runDaemon () {
|
|
|
1252
1261
|
try { ipcWrite(updatedFile, { v: 1, from: daemonVersion, to: r.version, at: Date.now() }) } catch (e) {
|
|
1253
1262
|
console.log(`[vault] update: ${r.version} installed, but the note to tell the approvers could not be written: ${e.message}`)
|
|
1254
1263
|
}
|
|
1255
|
-
|
|
1264
|
+
// Windows y macOS: hay que irse YA. En Windows el instalador cierra la bóveda y la vuelve
|
|
1265
|
+
// a arrancar; en macOS la levanta launchd o el relanzador que dejó la instalación.
|
|
1266
|
+
if (res.then === 'exit') {
|
|
1267
|
+
console.log(`[vault] update: ${r.version} installed · leaving so the new one starts`)
|
|
1268
|
+
shutdown(`updated to ${r.version}`)
|
|
1269
|
+
} else if (vigia) {
|
|
1256
1270
|
console.log(`[vault] update: ${r.version} installed · restarting now to run it`)
|
|
1257
1271
|
shutdown(`updated to ${r.version}`)
|
|
1258
1272
|
} else {
|
|
@@ -1263,6 +1277,7 @@ export async function runDaemon () {
|
|
|
1263
1277
|
} finally { actualizando = false }
|
|
1264
1278
|
}
|
|
1265
1279
|
let actualizando = false
|
|
1280
|
+
let avisadoOtra = null
|
|
1266
1281
|
let aprobada = null
|
|
1267
1282
|
let avisandoRoot = null
|
|
1268
1283
|
|
package/src/service.js
CHANGED
|
@@ -93,7 +93,11 @@ const guiDomain = () => `gui/${process.getuid?.() ?? 501}`
|
|
|
93
93
|
|
|
94
94
|
const xml = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
95
95
|
|
|
96
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* El LaunchAgent: lo arranca al entrar y lo relanza siempre que se va, como `Restart=always`
|
|
98
|
+
* de systemd. Hace falta también cuando sale bien: tras instalar la versión nueva, la bóveda
|
|
99
|
+
* se va para que el agente arranque la nueva. Para pararla de verdad, `autostart off`.
|
|
100
|
+
*/
|
|
97
101
|
export function launchAgentPlist ({ cmd, args }, log = daemonLogFile()) {
|
|
98
102
|
const argv = [cmd, ...args].map((a) => ` <string>${xml(a)}</string>`).join('\n')
|
|
99
103
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -109,16 +113,16 @@ ${argv}
|
|
|
109
113
|
<key>RunAtLoad</key>
|
|
110
114
|
<true/>
|
|
111
115
|
<key>KeepAlive</key>
|
|
112
|
-
<
|
|
113
|
-
<key>SuccessfulExit</key>
|
|
114
|
-
<false/>
|
|
115
|
-
</dict>
|
|
116
|
+
<true/>
|
|
116
117
|
<key>StandardOutPath</key>
|
|
117
118
|
<string>${xml(log)}</string>
|
|
118
119
|
<key>StandardErrorPath</key>
|
|
119
120
|
<string>${xml(log)}</string>
|
|
121
|
+
<!-- Standard y no Background: en Background macOS frena la red y la CPU, y la bóveda
|
|
122
|
+
atiende al teléfono (firmas, aprobaciones). Con Background una actualización tardaba
|
|
123
|
+
más de 5 minutos en bajar y la verificación se caía por tiempo. -->
|
|
120
124
|
<key>ProcessType</key>
|
|
121
|
-
<string>
|
|
125
|
+
<string>Standard</string>
|
|
122
126
|
</dict>
|
|
123
127
|
</plist>
|
|
124
128
|
`
|
package/src/tui/app.js
CHANGED
|
@@ -216,10 +216,11 @@ function offerUpdate (term, st) {
|
|
|
216
216
|
setConfirm(st, { text: i.updateOffer(r.version, VERSION), onYes: () => installUpdate(term, st, r) })
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
async function installUpdate (term, st, release, { install = up.
|
|
219
|
+
async function installUpdate (term, st, release, { install = (rel) => up.installRelease(rel), userInstall = up.canSelfUpdate } = {}) {
|
|
220
220
|
const i = L(st)
|
|
221
|
-
// Solo
|
|
222
|
-
// de
|
|
221
|
+
// Solo una instalación que puede cambiarse sola (la de usuario de Linux, el instalador de
|
|
222
|
+
// Windows, la app de macOS) se cambia desde aquí. Un paquete del sistema es de root: no se
|
|
223
|
+
// pide la contraseña por dentro, se dice el comando.
|
|
223
224
|
if (!userInstall()) { flash(st, i.updateNeedsCommand, 'warn'); return }
|
|
224
225
|
const r = await guard(term, st, i.updateInstalling(release.version), () => install(release))
|
|
225
226
|
if (!r.ok) return
|
|
@@ -228,6 +229,9 @@ async function installUpdate (term, st, release, { install = up.installUserRelea
|
|
|
228
229
|
flash(st, i.updateNoteFailed(e.message), 'warn')
|
|
229
230
|
}
|
|
230
231
|
st.update = { installed: r.v.version }
|
|
232
|
+
// WINDOWS: el instalador ya corre; cierra la bóveda y esta pantalla (son el mismo .exe), las
|
|
233
|
+
// cambia y vuelve a arrancar la bóveda. La ventana vuelve sola. No hay nada que preguntar.
|
|
234
|
+
if (r.v.then === 'exit' && process.platform === 'win32') { flash(st, i.updateWindowsInstalling(r.v.version), 'warn'); return }
|
|
231
235
|
// Reiniciar se PREGUNTA: cierra las bóvedas abiertas y corta lo que estén haciendo los
|
|
232
236
|
// aparatos. Si dices que no, el servicio se reinicia solo en un par de minutos.
|
|
233
237
|
setConfirm(st, {
|
package/src/tui/i18n.js
CHANGED
|
@@ -36,6 +36,7 @@ const es = {
|
|
|
36
36
|
updateInstalling: (version) => `Bajando la ${version} y comprobando la firma…`,
|
|
37
37
|
updateFailed: (reason) => `No se instaló nada: ${reason}`,
|
|
38
38
|
updateNeedsCommand: 'Esta instalación no se puede cambiar desde aquí. Sal y corre: dotrino-vault update',
|
|
39
|
+
updateWindowsInstalling: (v) => `Instalando la ${v}: esta pantalla se cierra un momento y la bóveda vuelve sola.`,
|
|
39
40
|
updateNoteFailed: (reason) => `Instalada, pero no se pudo dejar el aviso para tus aprobadores: ${reason}`,
|
|
40
41
|
updateRestartOffer: (version) => `Instalada la ${version}. ¿Reiniciar ahora la bóveda y esta pantalla? Cierra las bóvedas abiertas`,
|
|
41
42
|
updateRestarting: 'Reiniciando la bóveda…',
|
|
@@ -541,6 +542,7 @@ const en = {
|
|
|
541
542
|
updateInstalling: (version) => `Downloading ${version} and checking its signature…`,
|
|
542
543
|
updateFailed: (reason) => `Nothing was installed: ${reason}`,
|
|
543
544
|
updateNeedsCommand: 'This install cannot be changed from here. Quit and run: dotrino-vault update',
|
|
545
|
+
updateWindowsInstalling: (v) => `Installing ${v}: this screen closes for a moment and the vault comes back on its own.`,
|
|
544
546
|
updateNoteFailed: (reason) => `Installed, but the note for your approvers could not be written: ${reason}`,
|
|
545
547
|
updateRestartOffer: (version) => `${version} is installed. Restart the vault and this screen now? It locks the open vaults`,
|
|
546
548
|
updateRestarting: 'Restarting the vault…',
|
package/src/update.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import fs from 'node:fs'
|
|
13
13
|
import os from 'node:os'
|
|
14
14
|
import path from 'node:path'
|
|
15
|
-
import { execFileSync } from 'node:child_process'
|
|
15
|
+
import { execFileSync, spawn } from 'node:child_process'
|
|
16
16
|
import { latestVersion, isNewer, CHECK_EVERY_MS } from '@dotrino/update'
|
|
17
17
|
import { pickAsset, download, verifyArtifact as verify, fetchVerified } from '@dotrino/update/fetch'
|
|
18
18
|
import { VERSION } from './version.js'
|
|
@@ -137,3 +137,143 @@ async function instalarDesde (work, pick, release, { binDir, log, fetchVerifiedI
|
|
|
137
137
|
}
|
|
138
138
|
return { ok: true, version: release.version }
|
|
139
139
|
}
|
|
140
|
+
|
|
141
|
+
// ------------------------- Windows y macOS ---------------------------------
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* CÓMO ESTÁ INSTALADO ESTE BINARIO, y por tanto si puede actualizarse solo (CONVENCIONES §15).
|
|
145
|
+
*
|
|
146
|
+
* 'user' Linux, instalación de usuario (`~/.local/share/dotrino/bin`): cambia sus binarios.
|
|
147
|
+
* 'inno' Windows, el instalador por usuario (`unins000.exe` al lado): corre el instalador
|
|
148
|
+
* nuevo en silencio, que cierra la bóveda, la reemplaza y la vuelve a arrancar.
|
|
149
|
+
* 'app' macOS, dentro de `Dotrino Vault.app` en una carpeta que se puede escribir: se
|
|
150
|
+
* cambia la app entera (nunca un binario suelto: la app es un conjunto firmado).
|
|
151
|
+
* 'system' un paquete de root (.deb/.rpm), o una app en una carpeta que no es tuya.
|
|
152
|
+
* 'other' npm, Docker, un checkout: se actualiza por su propia vía.
|
|
153
|
+
*/
|
|
154
|
+
export function installKind ({ platform = process.platform, execPath = process.execPath, home = os.homedir(), exists = fs.existsSync, writable = canWrite } = {}) {
|
|
155
|
+
const sea = /^dotrino-vaultd(\.exe)?$/.test(path.basename(execPath))
|
|
156
|
+
if (!sea) return 'other'
|
|
157
|
+
if (platform === 'linux') return isUserInstall(execPath, home) ? 'user' : execPath.startsWith('/usr/') ? 'system' : 'other'
|
|
158
|
+
if (platform === 'win32') return exists(path.join(path.dirname(execPath), 'unins000.exe')) ? 'inno' : 'other'
|
|
159
|
+
if (platform === 'darwin') {
|
|
160
|
+
const app = appBundleOf(execPath)
|
|
161
|
+
if (!app) return 'other'
|
|
162
|
+
return writable(path.dirname(app)) ? 'app' : 'system'
|
|
163
|
+
}
|
|
164
|
+
return 'other'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** ¿Puede este binario instalar él mismo la versión nueva? */
|
|
168
|
+
export const canSelfUpdate = (opts) => ['user', 'inno', 'app'].includes(installKind(opts))
|
|
169
|
+
|
|
170
|
+
/** `/Applications/Dotrino Vault.app` a partir de su `Contents/MacOS/dotrino-vaultd`. */
|
|
171
|
+
export function appBundleOf (execPath) {
|
|
172
|
+
const m = /^(.*\.app)\/Contents\/MacOS\/dotrino-vaultd$/.exec(execPath)
|
|
173
|
+
return m ? m[1] : null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function canWrite (dir) { try { fs.accessSync(dir, fs.constants.W_OK); return true } catch { return false } }
|
|
177
|
+
|
|
178
|
+
/** Carpeta de trabajo de las descargas: la de los datos de Dotrino del usuario, no `/tmp`. */
|
|
179
|
+
function updateWorkDir (platform = process.platform) {
|
|
180
|
+
if (platform === 'win32') return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'Dotrino', 'update')
|
|
181
|
+
return path.join(os.homedir(), '.local', 'share', 'dotrino', 'update')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* INSTALA la versión nueva por la vía de este sistema. Lo que se baja se verifica contra la
|
|
186
|
+
* atestación del release antes de tocar nada; si no cuadra, no se toca nada y se dice.
|
|
187
|
+
*
|
|
188
|
+
* `then` dice qué tiene que hacer quien llamó:
|
|
189
|
+
* 'watch' Linux: el binario cambió en su sitio; el vigilante (o systemd) lo reinicia.
|
|
190
|
+
* 'exit' el proceso que corre la bóveda tiene que irse YA. En Windows el instalador lo
|
|
191
|
+
* cierra y lo vuelve a arrancar; en macOS lo levanta launchd o el relanzador.
|
|
192
|
+
*
|
|
193
|
+
* @returns {Promise<{ok:true, version:string, then:'watch'|'exit'} | {ok:false, code?:string, reason:string, file?:string}>}
|
|
194
|
+
*/
|
|
195
|
+
export async function installRelease (release, { log = () => {}, kind = installKind(), fetchVerifiedImpl = fetchVerified, run = spawnDetached, execPath = process.execPath, pid = null } = {}) {
|
|
196
|
+
if (kind === 'user') {
|
|
197
|
+
const r = await installUserRelease(release, { log, fetchVerifiedImpl })
|
|
198
|
+
return r.ok ? { ...r, then: 'watch' } : r
|
|
199
|
+
}
|
|
200
|
+
if (kind === 'inno') return installWindows(release, { log, fetchVerifiedImpl, run })
|
|
201
|
+
if (kind === 'app') return installMac(release, { log, fetchVerifiedImpl, run, execPath, pid })
|
|
202
|
+
return { ok: false, code: 'CANNOT_SELF_UPDATE', reason: kind === 'system' ? 'installed as a system package (root): update it with the system installer' : 'not installed with an installer: update it the way you installed it (npm, Docker…)' }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function spawnDetached (cmd, args, { log } = {}) {
|
|
206
|
+
const child = spawn(cmd, args, { detached: true, windowsHide: true, stdio: 'ignore' })
|
|
207
|
+
child.on('error', (e) => log?.(`[vault] update: could not start ${cmd}: ${e.message}`))
|
|
208
|
+
child.unref()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Baja y verifica un archivo del release a una carpeta propia (que se vacía antes). */
|
|
212
|
+
async function bajarVerificado (release, re, { log, fetchVerifiedImpl, work }) {
|
|
213
|
+
const pick = pickAsset(release.assets, [{ kind: 'asset', re }])
|
|
214
|
+
if (!pick.ok) return pick
|
|
215
|
+
fs.rmSync(work, { recursive: true, force: true })
|
|
216
|
+
fs.mkdirSync(work, { recursive: true, mode: 0o700 })
|
|
217
|
+
const got = await fetchVerifiedImpl(pick.asset, { repo: REPO, product: 'dotrino-vault', version: VERSION, dir: work })
|
|
218
|
+
if (got.ok) log(`[vault] update: ${pick.asset.name} verified against its attestation`)
|
|
219
|
+
return got
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* WINDOWS: el instalador nuevo, en silencio. Es lo único que puede cambiar los archivos: un
|
|
224
|
+
* .exe en marcha no se sobrescribe. El instalador cierra lo que los usa (la bóveda, y la
|
|
225
|
+
* ventana si está abierta), los reemplaza y vuelve a arrancar la bóveda (`--start-daemon`).
|
|
226
|
+
* Las tareas elegidas al instalar (arrancar al iniciar sesión) se conservan.
|
|
227
|
+
*/
|
|
228
|
+
async function installWindows (release, { log, fetchVerifiedImpl, run }) {
|
|
229
|
+
const work = updateWorkDir('win32')
|
|
230
|
+
const got = await bajarVerificado(release, /-windows-x64-setup\.exe$/, { log, fetchVerifiedImpl, work })
|
|
231
|
+
if (!got.ok) return got
|
|
232
|
+
run(got.file, ['/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/FORCECLOSEAPPLICATIONS', `/LOG=${path.join(work, 'setup.log')}`], { log })
|
|
233
|
+
log(`[vault] update: installer for ${release.version} started; it restarts the vault when it finishes`)
|
|
234
|
+
return { ok: true, version: release.version, then: 'exit' }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* macOS: la app entera. Se descomprime al lado de la actual (misma carpeta, así el cambio es
|
|
239
|
+
* un renombrado y no una copia a medias), se aparta la vieja, se pone la nueva y se borra la
|
|
240
|
+
* vieja. Lo que está corriendo sigue con su copia hasta que se reinicia.
|
|
241
|
+
*
|
|
242
|
+
* Quien la levanta: launchd si la bóveda corre como agente (KeepAlive); si corre suelta, un
|
|
243
|
+
* relanzador que espera a que este proceso se vaya y arranca el binario nuevo.
|
|
244
|
+
*/
|
|
245
|
+
async function installMac (release, { log, fetchVerifiedImpl, run, execPath, pid }) {
|
|
246
|
+
const app = appBundleOf(execPath)
|
|
247
|
+
if (!app) return { ok: false, code: 'CANNOT_SELF_UPDATE', reason: 'not running from Dotrino Vault.app' }
|
|
248
|
+
const work = updateWorkDir('darwin')
|
|
249
|
+
const got = await bajarVerificado(release, /-macos-universal\.zip$/, { log, fetchVerifiedImpl, work })
|
|
250
|
+
if (!got.ok) return got
|
|
251
|
+
const parent = path.dirname(app)
|
|
252
|
+
const stage = path.join(parent, `.dotrino-vault-update-${process.pid}`)
|
|
253
|
+
fs.rmSync(stage, { recursive: true, force: true })
|
|
254
|
+
try {
|
|
255
|
+
try { execFileSync('ditto', ['-x', '-k', got.file, stage], { stdio: ['ignore', 'ignore', 'pipe'] }) } catch (e) {
|
|
256
|
+
return { ok: false, code: 'UNPACK_FAILED', reason: `could not unpack the release: ${String(e.stderr || e.message).trim().split('\n').slice(-2).join(' · ')}` }
|
|
257
|
+
}
|
|
258
|
+
const nueva = path.join(stage, 'Dotrino Vault.app')
|
|
259
|
+
if (!fs.existsSync(path.join(nueva, 'Contents', 'MacOS', 'dotrino-vaultd'))) return { ok: false, code: 'BAD_ARCHIVE', reason: 'the archive has no Dotrino Vault.app inside' }
|
|
260
|
+
const vieja = `${app}.old-${process.pid}`
|
|
261
|
+
fs.renameSync(app, vieja)
|
|
262
|
+
try { fs.renameSync(nueva, app) } catch (e) {
|
|
263
|
+
fs.renameSync(vieja, app) // se deja como estaba
|
|
264
|
+
return { ok: false, code: 'SWAP_FAILED', reason: `could not put the new app in place: ${e.message}` }
|
|
265
|
+
}
|
|
266
|
+
fs.rmSync(vieja, { recursive: true, force: true })
|
|
267
|
+
} finally {
|
|
268
|
+
fs.rmSync(stage, { recursive: true, force: true })
|
|
269
|
+
fs.rmSync(work, { recursive: true, force: true })
|
|
270
|
+
}
|
|
271
|
+
// Suelta (sin launchd): el relanzador espera a que `pid` se vaya y arranca el nuevo.
|
|
272
|
+
if (pid && process.env.XPC_SERVICE_NAME !== 'com.dotrino.vault') {
|
|
273
|
+
const bin = path.join(app, 'Contents', 'MacOS', 'dotrino-vaultd')
|
|
274
|
+
const logFile = path.join(os.homedir(), '.local', 'share', 'dotrino', 'vault', 'daemon.log')
|
|
275
|
+
run('/bin/sh', ['-c', 'while kill -0 "$1" 2>/dev/null; do sleep 0.5; done; exec "$2" >>"$3" 2>&1', 'relaunch', String(pid), bin, logFile], { log })
|
|
276
|
+
}
|
|
277
|
+
log(`[vault] update: ${release.version} installed in ${app}`)
|
|
278
|
+
return { ok: true, version: release.version, then: 'exit' }
|
|
279
|
+
}
|