@dotrino/vaultd 0.152.0 → 0.153.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vaultd",
3
- "version": "0.152.0",
3
+ "version": "0.153.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": {
@@ -25,7 +25,7 @@
25
25
  "@dotrino/identity": "^0.111.0",
26
26
  "@dotrino/opaque": "0.1.0",
27
27
  "@dotrino/passmanager": "0.17.0",
28
- "@dotrino/proxy-client": "0.30.1",
28
+ "@dotrino/proxy-client": "^0.31.1",
29
29
  "@dotrino/roadmap": "0.3.19",
30
30
  "@dotrino/store": "0.11.0",
31
31
  "@dotrino/update": "0.5.1",
package/src/ctl.js CHANGED
@@ -1832,8 +1832,8 @@ async function waitForService (version, { timeoutMs = 4 * 60_000 } = {}) {
1832
1832
  * marcador que deja el servicio cuando se actualiza solo. Si no se puede escribir se dice:
1833
1833
  * la actualización está hecha, lo que se pierde es el aviso.
1834
1834
  */
1835
- function noteUpdated (to) {
1836
- try { ipcWrite(path.join(dir, 'updated.json'), { v: 1, from: readState().version || VERSION, to, at: Date.now() }) } catch (e) {
1835
+ async function noteUpdated (to) {
1836
+ try { (await import('./update.js')).noteUpdated(dir, { from: readState().version || VERSION, to }) } catch (e) {
1837
1837
  console.error('No se pudo dejar el aviso de actualización para tus aprobadores: %s', e.message)
1838
1838
  }
1839
1839
  }
@@ -1883,8 +1883,23 @@ async function cmdUpdate (args = []) {
1883
1883
  }
1884
1884
  console.log('firmada por el release de %s ✓', 'imdotrino/dotrino-vault')
1885
1885
  console.log('Instalada la %s.', res.version)
1886
- noteUpdated(res.version)
1887
- if (!args.includes('--no-wait')) await waitForService(res.version)
1886
+ await noteUpdated(res.version)
1887
+ if (args.includes('--no-wait')) return
1888
+ // AL TERMINAR SE OFRECE REINICIAR (dueño, 2026-10-13). El servicio se reinicia solo al
1889
+ // ver el binario nuevo, pero espera a verlo quieto dos minutos: quien está delante puede
1890
+ // no querer esperar. Solo se pregunta con alguien delante y con systemd; si no, se espera.
1891
+ const s = ipcRead(stateFile, null)
1892
+ if (s && alive(s.pid) && s.version !== res.version && process.stdin.isTTY && process.platform === 'linux' && !IN_DOCKER) {
1893
+ const resp = (await askText('¿Reiniciar la bóveda ahora para usarla? Cierra las bóvedas abiertas. [s/N] ')).trim().toLowerCase()
1894
+ if (resp === 's' || resp === 'y') {
1895
+ try { execFileSync('systemctl', ['--user', 'restart', 'dotrino-vault'], { stdio: 'inherit' }) } catch (e) {
1896
+ console.error('No se pudo reiniciar: %s', e.message)
1897
+ console.error('Hazlo a mano: %s', RESTART_HINT)
1898
+ process.exit(1)
1899
+ }
1900
+ }
1901
+ }
1902
+ await waitForService(res.version)
1888
1903
  return
1889
1904
  }
1890
1905
 
@@ -1924,7 +1939,7 @@ async function cmdUpdate (args = []) {
1924
1939
  B, file, r.version, Z)
1925
1940
  return
1926
1941
  }
1927
- noteUpdated(r.version)
1942
+ await noteUpdated(r.version)
1928
1943
  console.log('Listo. El servicio se reinicia solo; compruébalo con: dotrino-vault status')
1929
1944
  }
1930
1945
 
package/src/tui/app.js CHANGED
@@ -30,7 +30,7 @@
30
30
  * que entrar antes), el candado es `k` (la `l` es el idioma) y la contraseña `c`.
31
31
  */
32
32
  import fs from 'node:fs'
33
- import { execFile } from 'node:child_process'
33
+ import { execFile, spawnSync } from 'node:child_process'
34
34
  import { parseEnvInput } from '../../lib/src/envtext.js'
35
35
  import { createTerm, widthOf } from './term.js'
36
36
  import { qrToString } from '../qr.js'
@@ -39,6 +39,7 @@ import * as vc from '../vaultControl.js'
39
39
  import { readSettings, writeSettings } from '../settings.js'
40
40
  import * as hw from '../hwkeys.js'
41
41
  import { VERSION } from '../version.js'
42
+ import * as up from '../update.js'
42
43
  import { DEVICE_CAPS } from '@dotrino/identity/acta'
43
44
 
44
45
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
@@ -184,6 +185,65 @@ function lockGlyph (p) {
184
185
  return p.locked ? '🔒' : '🔓'
185
186
  }
186
187
 
188
+ function restartDaemonService () {
189
+ return new Promise((res, rej) => {
190
+ execFile('systemctl', ['--user', 'restart', 'dotrino-vault'], { timeout: 20000 }, (err, so, se) => {
191
+ if (err) rej(new Error(String(se || '').trim() || err.message)); else res()
192
+ })
193
+ })
194
+ }
195
+
196
+ /**
197
+ * AL ENTRAR SE DICE SI HAY VERSIÓN NUEVA, Y SE OFRECE INSTALARLA (dueño, 2026-10-13: como
198
+ * hace la app de Terminal). El servicio ya se actualiza solo; esto es para quien está
199
+ * mirando: enterarse ahí mismo, instalarla sin salir y, al terminar, reiniciar.
200
+ *
201
+ * Se mira EN SEGUNDO PLANO: la pantalla sale al momento y el aviso llega cuando el registro
202
+ * contesta. «No se pudo mirar» no es «estás al día»: no se dice nada y queda en `st.update`.
203
+ */
204
+ function checkForUpdate (st, { latest = up.latestRelease } = {}) {
205
+ if (VERSION === 'dev') return
206
+ st.update = { checking: true }
207
+ Promise.resolve().then(() => latest()).then((r) => {
208
+ if (!r?.ok) { st.update = { failed: r?.reason || 'unknown' }; return }
209
+ st.update = up.isNewer(r.version, VERSION) ? { release: r, offer: true } : { current: true }
210
+ }).catch((e) => { st.update = { failed: e?.message || String(e) } })
211
+ }
212
+
213
+ /** Pregunta UNA vez por sesión. Decir que no deja el aviso arriba, sin volver a preguntar. */
214
+ function offerUpdate (term, st) {
215
+ const i = L(st)
216
+ const r = st.update.release
217
+ st.update = { release: r }
218
+ setConfirm(st, { text: i.updateOffer(r.version, VERSION), onYes: () => installUpdate(term, st, r) })
219
+ }
220
+
221
+ async function installUpdate (term, st, release, { install = up.installUserRelease, userInstall = up.isUserInstall } = {}) {
222
+ const i = L(st)
223
+ // Solo la instalación de usuario se puede cambiar desde aquí. Un paquete del sistema es
224
+ // de root: no se pide la contraseña por dentro, se dice el comando.
225
+ if (!userInstall()) { flash(st, i.updateNeedsCommand, 'warn'); return }
226
+ const r = await guard(term, st, i.updateInstalling(release.version), () => install(release))
227
+ if (!r.ok) return
228
+ if (!r.v.ok) { flash(st, i.updateFailed(r.v.reason), 'danger'); return }
229
+ try { up.noteUpdated(vc.vaultDir(), { from: st.state?.version || VERSION, to: r.v.version }) } catch (e) {
230
+ flash(st, i.updateNoteFailed(e.message), 'warn')
231
+ }
232
+ st.update = { installed: r.v.version }
233
+ // Reiniciar se PREGUNTA: cierra las bóvedas abiertas y corta lo que estén haciendo los
234
+ // aparatos. Si dices que no, el servicio se reinicia solo en un par de minutos.
235
+ setConfirm(st, {
236
+ text: i.updateRestartOffer(r.v.version),
237
+ onYes: async () => {
238
+ const rr = await guard(term, st, i.updateRestarting, () => restartDaemonService())
239
+ if (!rr.ok) return
240
+ st.relaunch = true
241
+ st.quit = true
242
+ },
243
+ onNo: () => { flash(st, i.updateRestartLater(r.v.version)) }
244
+ })
245
+ }
246
+
187
247
  function startDaemonService () {
188
248
  return new Promise((res) => {
189
249
  execFile('systemctl', ['--user', 'start', 'dotrino-vault'], { timeout: 8000 }, (err, so, se) => {
@@ -2502,6 +2562,8 @@ function render (term, st) {
2502
2562
  ? t.muted(i.updateWaiting(u.version, dia(u.askedAt + 24 * 60 * 60 * 1000)))
2503
2563
  : t.warn(i.updateNotApproved(u.version, dia(u.askedAt))))
2504
2564
  }
2565
+ // Hay versión nueva y se dijo que no (o todavía no se preguntó): se queda dicho aquí.
2566
+ if (!lines[2] && st.update?.release) lines[2] = ' ' + t.warn(i.updateAvailable(st.update.release.version))
2505
2567
  // Dispositivos/Scopes son pestañas de la bóveda activa (se entra desde Bóvedas);
2506
2568
  // el resto muestra su título simple.
2507
2569
  lines[3] = INNER_TABS.includes(st.screen) ? ' ' + renderTabs(st, t) : ' ' + t.title('» ' + title(i, st.screen))
@@ -2642,11 +2704,14 @@ export async function runTui () {
2642
2704
  st.state = vc.readState()
2643
2705
  st.daemonUp = true
2644
2706
  await refreshAll(term, st)
2707
+ checkForUpdate(st)
2645
2708
 
2646
2709
  let running = true
2647
2710
  while (running) {
2648
2711
  st.state = vc.readState()
2649
2712
  st.daemonUp = vc.daemonAlive()
2713
+ // El aviso de versión nueva sale cuando no hay otra cosa preguntando.
2714
+ if (st.update?.offer && !st.input && !st.confirm) offerUpdate(term, st)
2650
2715
  // ¿Se cerró sola mientras nadie miraba? Olvida su contraseña y sal de su contenido.
2651
2716
  await forgetAutoLocked(term, st)
2652
2717
  // caducar el flash a los ~4 s
@@ -2656,7 +2721,10 @@ export async function runTui () {
2656
2721
  const base = (st.screen === 'pairing' || st.screen === 'join' || (st.screen === 'devices' && !st.input && !st.confirm)) ? 800 : 0
2657
2722
  // Despertar a tiempo del bloqueo automático, además del sondeo de la pantalla.
2658
2723
  const wake = autoLockWakeIn(st)
2659
- const tick = base && wake ? Math.min(base, wake) : (base || wake)
2724
+ let tick = base && wake ? Math.min(base, wake) : (base || wake)
2725
+ // Mientras se mira si hay versión nueva hay que despertar sin tecla, o el aviso no
2726
+ // saldría hasta que tocaras algo.
2727
+ if (st.update?.checking) tick = tick ? Math.min(tick, 500) : 500
2660
2728
  const key = await term.readKey(tick)
2661
2729
 
2662
2730
  if (key.name === 'resize') continue
@@ -2728,7 +2796,13 @@ export async function runTui () {
2728
2796
  for (const id of st.unlockedHere) { try { await vc.lockProfile(id) } catch (_) {} }
2729
2797
  term.close()
2730
2798
  }
2799
+ // TRAS ACTUALIZAR Y REINICIAR, esta pantalla también se vuelve a abrir: es el binario
2800
+ // anterior el que sigue en memoria. Se lanza el nuevo en esta misma terminal.
2801
+ if (st.relaunch) {
2802
+ const r = spawnSync(process.execPath, process.argv.slice(1), { stdio: 'inherit' })
2803
+ process.exit(r.status ?? 0)
2804
+ }
2731
2805
  }
2732
2806
 
2733
2807
  // Solo para pruebas headless (render sin terminal real). No usar en runtime.
2734
- export const __test = { render, settingsRows, onKeySettings, doorRows, onKeyDoors, pairCapsRows, onKeyPairCaps, onKeyDevices, draftChanges, saveCapsDrafts, onKeySecrets, loginRows, onKeyLogins, refreshLogins, activeLocked, autoLockedIds, autoLockWakeIn, forgetAutoLocked, autoLockMin, refreshAll, ensureUnlocked, profileRows, deviceRows, secretRows, devVarRows, meRows, capsRows, onKeyCaps, pairModeRows, pairingBody, scrollBody, fitHelp, wrapHelp, wrapWords, joinBody, onKeyJoin, promptJoin, onKeyProfiles, onInputKey, toggleLang, mergeMembersAndCerts, seguirAqui, resetToque: () => { ultimoToque = 0 } }
2808
+ export const __test = { checkForUpdate, offerUpdate, installUpdate, render, settingsRows, onKeySettings, doorRows, onKeyDoors, pairCapsRows, onKeyPairCaps, onKeyDevices, draftChanges, saveCapsDrafts, onKeySecrets, loginRows, onKeyLogins, refreshLogins, activeLocked, autoLockedIds, autoLockWakeIn, forgetAutoLocked, autoLockMin, refreshAll, ensureUnlocked, profileRows, deviceRows, secretRows, devVarRows, meRows, capsRows, onKeyCaps, pairModeRows, pairingBody, scrollBody, fitHelp, wrapHelp, wrapWords, joinBody, onKeyJoin, promptJoin, onKeyProfiles, onInputKey, toggleLang, mergeMembersAndCerts, seguirAqui, resetToque: () => { ultimoToque = 0 } }
package/src/tui/i18n.js CHANGED
@@ -28,6 +28,15 @@ const es = {
28
28
  daemonStopped: 'DETENIDO',
29
29
  updateWaiting: (version, until) => `⬆ la ${version} espera tu aprobación hasta ${until} UTC: mira Pedidos en tu teléfono`,
30
30
  updateNotApproved: (version, when) => `⬆ se pidió permiso para la ${version} el ${when} UTC y no se aprobó: no se vuelve a pedir · dotrino-vault update`,
31
+ updateAvailable: (version) => `⬆ hay la versión ${version}: instálala con dotrino-vault update`,
32
+ updateOffer: (version, current) => `Hay la versión ${version} y esta es la ${current}. ¿Instalarla ahora?`,
33
+ updateInstalling: (version) => `Bajando la ${version} y comprobando la firma…`,
34
+ updateFailed: (reason) => `No se instaló nada: ${reason}`,
35
+ updateNeedsCommand: 'Esta instalación no se puede cambiar desde aquí. Sal y corre: dotrino-vault update',
36
+ updateNoteFailed: (reason) => `Instalada, pero no se pudo dejar el aviso para tus aprobadores: ${reason}`,
37
+ updateRestartOffer: (version) => `Instalada la ${version}. ¿Reiniciar ahora la bóveda y esta pantalla? Cierra las bóvedas abiertas`,
38
+ updateRestarting: 'Reiniciando la bóveda…',
39
+ updateRestartLater: (version) => `La ${version} queda instalada: la bóveda se reinicia sola en un par de minutos`,
31
40
  daemonStale: (running, installed) => `⚠ el servicio corre ${running} y el binario instalado es ${installed}: reinícialo (systemctl --user restart dotrino-vault)`,
32
41
  activeVault: 'Bóveda activa: ',
33
42
  noName: '(sin nombre)',
@@ -516,6 +525,15 @@ const en = {
516
525
  daemonStopped: 'STOPPED',
517
526
  updateWaiting: (version, until) => `⬆ ${version} is waiting for your approval until ${until} UTC: check Requests on your phone`,
518
527
  updateNotApproved: (version, when) => `⬆ approval for ${version} was asked on ${when} UTC and not given: it will not ask again · dotrino-vault update`,
528
+ updateAvailable: (version) => `⬆ version ${version} is out: install it with dotrino-vault update`,
529
+ updateOffer: (version, current) => `Version ${version} is out and this is ${current}. Install it now?`,
530
+ updateInstalling: (version) => `Downloading ${version} and checking its signature…`,
531
+ updateFailed: (reason) => `Nothing was installed: ${reason}`,
532
+ updateNeedsCommand: 'This install cannot be changed from here. Quit and run: dotrino-vault update',
533
+ updateNoteFailed: (reason) => `Installed, but the note for your approvers could not be written: ${reason}`,
534
+ updateRestartOffer: (version) => `${version} is installed. Restart the vault and this screen now? It locks the open vaults`,
535
+ updateRestarting: 'Restarting the vault…',
536
+ updateRestartLater: (version) => `${version} is installed: the vault restarts by itself in a couple of minutes`,
519
537
  daemonStale: (running, installed) => `⚠ the service runs ${running} but the installed binary is ${installed}: restart it (systemctl --user restart dotrino-vault)`,
520
538
  activeVault: 'Active vault: ',
521
539
  tooSmall: 'Terminal too small',
package/src/update.js CHANGED
@@ -16,11 +16,22 @@ import { execFileSync } 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'
19
+ import { ipcWrite } from './ipc.js'
19
20
 
20
21
  export const REPO = 'imdotrino/dotrino-vault'
21
22
  export { isNewer, CHECK_EVERY_MS, download }
22
23
 
23
24
  /** La última release del vault. `{ ok:false, reason }` si no se pudo mirar, que NO es «al día». */
25
+ /**
26
+ * DEJA DICHO QUE SE INSTALÓ una versión nueva, para que el servicio se lo cuente a quien
27
+ * aprueba cuando arranque con ella (`anunciarActualizacion` en daemon.js). Es el mismo
28
+ * marcador que deja el servicio cuando se actualiza solo, y lo dejan igual el comando
29
+ * `update` y la TUI.
30
+ */
31
+ export function noteUpdated (dir, { from, to }) {
32
+ ipcWrite(path.join(dir, 'updated.json'), { v: 1, from, to, at: Date.now() })
33
+ }
34
+
24
35
  export const latestRelease = (opts = {}) =>
25
36
  latestVersion({ source: 'github', repo: REPO, product: 'dotrino-vault', version: VERSION, ...opts })
26
37