@dotrino/vaultd 0.156.0 → 0.158.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/README.md +56 -14
- package/bin/sea-entry.js +4 -0
- package/package.json +3 -2
- package/src/ctl.js +72 -2
- package/src/daemon.js +21 -6
- package/src/service.js +251 -0
- package/src/tui/app.js +326 -130
- package/src/tui/gui.js +214 -0
- package/src/tui/i18n.js +27 -6
- package/src/tui/term.js +7 -2
- package/src/update.js +146 -2
package/README.md
CHANGED
|
@@ -128,10 +128,49 @@ firmado**. Se lee con `dotrino-vault activity`.
|
|
|
128
128
|
|
|
129
129
|
## Instalación
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
Todas las vías dejan la misma bóveda: **instalador** de Linux (`.deb`, `.rpm`,
|
|
132
|
+
tarball), de **Windows** (`-setup.exe`) y de **macOS** (`Dotrino Vault.app`), **un
|
|
133
|
+
comando** (`npx`, en cualquier sistema con Node) y **Docker**. Los instaladores traen
|
|
134
|
+
además **la ventana** (ver abajo); `npx` y Docker, la TUI.
|
|
135
|
+
|
|
136
|
+
### La ventana (`desktop/`)
|
|
137
|
+
|
|
138
|
+
Una app de escritorio con **las mismas pantallas que la TUI**: bóvedas, aparatos,
|
|
139
|
+
emparejar (con el QR como imagen), permisos, cajones y variables, inicios de sesión, mi
|
|
140
|
+
perfil, llaves de seguridad y ajustes. No reimplementa nada: lanza `dotrino-vaultd
|
|
141
|
+
--gui-bridge` y pinta el modelo que le manda el puente (`src/tui/gui.js`), que corre la
|
|
142
|
+
misma sesión que la TUI. Una acción nueva en la TUI aparece en la ventana sin tocarla. Sin
|
|
143
|
+
puertos: el canal son las tuberías del proceso hijo. Rust + `iced`, con el tema del
|
|
144
|
+
ecosistema (Quicksand/Comfortaa, claro u oscuro según el sistema).
|
|
145
|
+
|
|
146
|
+
Busca la bóveda al lado de su ejecutable (así la ponen los instaladores), luego en el
|
|
147
|
+
`PATH` (`dotrino-vaultd`, o el `dotrino-vault` de npm). Para desarrollo:
|
|
148
|
+
`DOTRINO_VAULT_BRIDGE="node bin/dotrino-vault.js" cargo run` en `desktop/`.
|
|
149
|
+
|
|
150
|
+
**Windows — instalador:** `dotrino-vault-X.Y.Z-windows-x64-setup.exe` desde
|
|
151
|
+
[Releases](https://github.com/imdotrino/dotrino-vault/releases/latest). Se instala para
|
|
152
|
+
tu usuario, sin administrador, en `%LOCALAPPDATA%\Programs\Dotrino Vault`, con su entrada
|
|
153
|
+
en el menú Inicio, y deja la bóveda **arrancando al iniciar sesión** (una entrada `Run`
|
|
154
|
+
que lanza `dotrino-vault-desktop.exe --start-daemon`, sin ventana). Se cambia en Ajustes o
|
|
155
|
+
con `dotrino-vault service autostart on|off`. Desinstalar no borra tus datos.
|
|
156
|
+
|
|
157
|
+
**macOS — `Dotrino Vault.app`:** el `.zip` universal (Apple Silicon e Intel) desde
|
|
158
|
+
[Releases](https://github.com/imdotrino/dotrino-vault/releases/latest); arrástrala a
|
|
159
|
+
Aplicaciones. Dentro van la ventana y la bóveda. Para que arranque al iniciar sesión, en
|
|
160
|
+
Ajustes (un LaunchAgent de tu usuario, `~/Library/LaunchAgents/com.dotrino.vault.plist`).
|
|
161
|
+
|
|
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`.
|
|
135
174
|
|
|
136
175
|
**Ubuntu / Debian — `.deb`:** descarga el `.deb` (versionado) desde
|
|
137
176
|
[Releases](https://github.com/imdotrino/dotrino-vault/releases/latest) y haz doble
|
|
@@ -196,7 +235,7 @@ npx -y @dotrino/vaultd # Node ≥ 20
|
|
|
196
235
|
npx -y @dotrino/vaultd --tui # la bóveda y su pantalla de control en la misma ventana
|
|
197
236
|
```
|
|
198
237
|
|
|
199
|
-
|
|
238
|
+
Sirve en cualquier sistema (y es la vía en ARM fuera de Docker). Arranca en primer
|
|
200
239
|
plano: la bóveda vive mientras dejes esa ventana abierta. Los comandos de control se
|
|
201
240
|
anteponen con `npx -p @dotrino/vaultd` (por ejemplo, `npx -p @dotrino/vaultd
|
|
202
241
|
dotrino-vault pair`). Si no tienes Node, el instalador del ecosistema lo baja en tu
|
|
@@ -228,9 +267,9 @@ recupera.
|
|
|
228
267
|
|
|
229
268
|
> Sin firma de código: tu sistema puede advertir que el binario no está firmado. Es
|
|
230
269
|
> autohospedado y de código abierto; en Linux solo necesita permiso de ejecución (el
|
|
231
|
-
> instalador lo da).
|
|
232
|
-
>
|
|
233
|
-
>
|
|
270
|
+
> instalador lo da). El daemon de Windows (`packaging/build-win.sh`) se construye desde
|
|
271
|
+
> Linux y va dentro del instalador de Windows; el de macOS (`packaging/build-mac.sh`) se
|
|
272
|
+
> construye en un Mac y va dentro de la app.
|
|
234
273
|
|
|
235
274
|
**Dónde vive todo y cómo se para.** Tus datos —llave incluida— viven en
|
|
236
275
|
`~/.local/share/dotrino/vault` (Linux y macOS), `%LOCALAPPDATA%\Dotrino\vault`
|
|
@@ -848,8 +887,10 @@ ed25519 en formato OpenSSH (sin frase: la bóveda es el candado) y RSA/P-256 en
|
|
|
848
887
|
## Alcance
|
|
849
888
|
|
|
850
889
|
- **v1 (este):** daemon headless en Node, **multi-perfil**. En **Linux** queda como
|
|
851
|
-
servicio `systemd --user` (binario único Node SEA: `.deb` y tarball); en
|
|
852
|
-
|
|
890
|
+
servicio `systemd --user` (binario único Node SEA: `.deb`, `.rpm` y tarball); en
|
|
891
|
+
**Windows** con un instalador por usuario y en **macOS** como `Dotrino Vault.app`, los
|
|
892
|
+
dos con arranque al iniciar sesión; en los tres, con **una ventana** que tiene las
|
|
893
|
+
pantallas de la TUI (`desktop/`); y en cualquier sistema, con `npx` o **Docker**
|
|
853
894
|
(imagen amd64/arm64 en GHCR). Identidad **cifrada en reposo y ligada a esta
|
|
854
895
|
máquina**. Emparejamiento endurecido con los dos caminos de vinculación, acta del
|
|
855
896
|
perfil (miembros, capacidades y `cn` de servicio), firma delegada, renovación
|
|
@@ -858,9 +899,8 @@ ed25519 en formato OpenSSH (sin frase: la bóveda es el candado) y RSA/P-256 en
|
|
|
858
899
|
actividad. Contraseña opcional por perfil, **para editarlo** (verificador PBKDF2).
|
|
859
900
|
- **v2:** cifrado en reposo **con la contraseña del perfil** (hoy la clave sale solo de
|
|
860
901
|
la máquina) y atado a tu cuenta del sistema —DPAPI en Windows, Keychain en macOS— o
|
|
861
|
-
al TPM; cifrar también los secretos y el store;
|
|
862
|
-
|
|
863
|
-
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`).
|
|
864
904
|
|
|
865
905
|
## Estructura
|
|
866
906
|
|
|
@@ -870,7 +910,9 @@ ed25519 en formato OpenSSH (sin frase: la bóveda es el candado) y RSA/P-256 en
|
|
|
870
910
|
- `src/daemon.js` — modo servicio: `state.json`, control por archivos + señales, apagado limpio.
|
|
871
911
|
- `src/ctl.js` — CLI de control (habla con el daemon por archivos + señales, sin socket).
|
|
872
912
|
- `src/vaultControl.js` — API de control programática (misma vía que la CLI); la usa la TUI.
|
|
873
|
-
- `src/tui/` — interfaz de terminal, sin dependencias (`term.js` primitivas ANSI/raw-mode; `app.js` pantallas; `i18n.js` los textos es/en).
|
|
913
|
+
- `src/tui/` — interfaz de terminal, sin dependencias (`term.js` primitivas ANSI/raw-mode; `app.js` pantallas; `i18n.js` los textos es/en; `gui.js` el puente de la ventana).
|
|
914
|
+
- `src/service.js` — arrancar, reiniciar y arrancar al iniciar sesión en Linux, Windows y macOS.
|
|
915
|
+
- `desktop/` — la ventana (Rust + `iced`): pinta lo que le manda `gui.js`.
|
|
874
916
|
- `src/transport.js` — conexión headless al proxy + `identify` firmado (con el acta).
|
|
875
917
|
- `src/threadStore.js` — el store del usuario: hilos, "recientes" y perfil (`threads.json`).
|
|
876
918
|
- `src/store.js` — árbol de contenidos (`vault.json`, versionado). Hoy es un esqueleto: se lee, nadie escribe.
|
package/bin/sea-entry.js
CHANGED
|
@@ -17,6 +17,10 @@ if (argv[0] === '--tui') {
|
|
|
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
19
|
.catch((e) => { console.error('error en la TUI:', e.message); process.exit(1) })
|
|
20
|
+
} else if (argv[0] === '--gui-bridge') {
|
|
21
|
+
// El canal de la ventana de escritorio (JSON por stdin/stdout, ver src/tui/gui.js).
|
|
22
|
+
import('../src/tui/gui.js').then(({ runGuiBridge }) => runGuiBridge()).then(() => process.exit(0))
|
|
23
|
+
.catch((e) => { console.error('error en el puente de la ventana:', e.message); process.exit(1) })
|
|
20
24
|
} else if (argv[0] === '--ctl') {
|
|
21
25
|
// CLI de control: NO toca la identidad ni el proxy; habla con el daemon vía
|
|
22
26
|
// el dir de datos (state.json) y señales (SIGUSR1 para emparejar).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.158.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,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
|
@@ -102,7 +102,7 @@ const START_HINT = IN_DOCKER
|
|
|
102
102
|
? 'docker start dotrino-vault'
|
|
103
103
|
: process.platform === 'linux'
|
|
104
104
|
? 'systemctl --user start dotrino-vault'
|
|
105
|
-
: 'dotrino-
|
|
105
|
+
: 'dotrino-vault service start (o: npx -y @dotrino/vaultd)'
|
|
106
106
|
const RESTART_HINT = IN_DOCKER
|
|
107
107
|
? 'docker restart dotrino-vault'
|
|
108
108
|
: process.platform === 'linux'
|
|
@@ -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… ')
|
|
@@ -2240,6 +2272,36 @@ function cmdLogs () {
|
|
|
2240
2272
|
}
|
|
2241
2273
|
|
|
2242
2274
|
// Import dinámico: así `dotrino-vault status` (el caso común) no carga la TUI.
|
|
2275
|
+
/**
|
|
2276
|
+
* `service start | autostart [on|off]`: arrancar el daemon y dejarlo arrancando al iniciar
|
|
2277
|
+
* sesión, en los tres sistemas (`src/service.js`). En Linux es la unidad de systemd; en
|
|
2278
|
+
* Windows una entrada de inicio de sesión; en macOS un LaunchAgent.
|
|
2279
|
+
*/
|
|
2280
|
+
async function cmdService (rest) {
|
|
2281
|
+
const sv = await import('./service.js')
|
|
2282
|
+
const [sub, val] = rest
|
|
2283
|
+
if (sub === 'start') {
|
|
2284
|
+
// `readState()` sale si no hay state.json, y sin state.json es justo cuando hay que arrancar.
|
|
2285
|
+
const s = ipcRead(stateFile, null)
|
|
2286
|
+
if (s && alive(s.pid)) { console.log('ya está corriendo (pid ' + s.pid + ')'); return }
|
|
2287
|
+
const r = await sv.startDaemonService()
|
|
2288
|
+
if (!r.ok) { console.error('no se pudo arrancar: ' + r.err); process.exit(1) }
|
|
2289
|
+
console.log('arrancado · registro: ' + sv.daemonLogFile())
|
|
2290
|
+
return
|
|
2291
|
+
}
|
|
2292
|
+
if (sub === 'autostart') {
|
|
2293
|
+
if (val === 'on' || val === 'off') {
|
|
2294
|
+
const r = val === 'on' ? await sv.enableAutostart() : await sv.disableAutostart()
|
|
2295
|
+
if (!r.ok) { console.error('no se pudo: ' + r.err); process.exit(1) }
|
|
2296
|
+
} else if (val !== undefined) { console.error('uso: dotrino-vault service autostart [on|off]'); process.exit(2) }
|
|
2297
|
+
const st = await sv.autostartStatus()
|
|
2298
|
+
console.log('arranca al iniciar sesión: ' + (st.on === null ? 'no se sabe en este sistema' : st.on ? 'sí' : 'no') + (st.how !== 'none' ? ` (${st.how})` : ''))
|
|
2299
|
+
return
|
|
2300
|
+
}
|
|
2301
|
+
console.error('uso: dotrino-vault service start | autostart [on|off]')
|
|
2302
|
+
process.exit(2)
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2243
2305
|
async function cmdTui () {
|
|
2244
2306
|
if (!process.stdout.isTTY) { console.error('la TUI necesita un terminal interactivo (TTY).'); process.exit(2) }
|
|
2245
2307
|
const { runTui } = await import('./tui/app.js')
|
|
@@ -2250,6 +2312,10 @@ function help () {
|
|
|
2250
2312
|
console.log(`dotrino-vault — control del certificador personal
|
|
2251
2313
|
|
|
2252
2314
|
tui interfaz de terminal a pantalla completa (bóvedas, pares, secretos)
|
|
2315
|
+
service start arranca el servicio en segundo plano (sin ventana)
|
|
2316
|
+
service autostart [on|off]
|
|
2317
|
+
que arranque solo al iniciar sesión (systemd en Linux, una entrada
|
|
2318
|
+
de inicio en Windows, un LaunchAgent en macOS) · sin valor, dice cómo está
|
|
2253
2319
|
status | info estado del servicio + fingerprint + el id de cada perfil
|
|
2254
2320
|
update [--check] trae la versión nueva (verificando la firma) y espera a que el
|
|
2255
2321
|
servicio la arranque · --check solo mira · --no-wait no espera
|
|
@@ -2485,6 +2551,10 @@ export async function runCtl (argv) {
|
|
|
2485
2551
|
if (rest.includes('--help') || rest.includes('-h')) return help()
|
|
2486
2552
|
switch (cmd) {
|
|
2487
2553
|
case 'tui': return cmdTui()
|
|
2554
|
+
// El canal de la ventana de escritorio (`desktop/`). No es para personas: habla JSON por
|
|
2555
|
+
// stdin/stdout. Ver `src/tui/gui.js`.
|
|
2556
|
+
case 'gui-bridge': { await (await import('./tui/gui.js')).runGuiBridge(); process.exit(0) }
|
|
2557
|
+
case 'service': return cmdService(rest)
|
|
2488
2558
|
case 'profile': return cmdProfile(rest)
|
|
2489
2559
|
case 'unlock': return cmdUnlock(rest)
|
|
2490
2560
|
case 'lock': return cmdLock()
|
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
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* service.js — arrancar, reiniciar y dejar arrancando al iniciar sesión el daemon del vault,
|
|
3
|
+
* en los tres sistemas.
|
|
4
|
+
*
|
|
5
|
+
* Lo usan la TUI y la ventana (las dos ofrecen «arrancar el servicio» cuando no está) y el
|
|
6
|
+
* CLI (`dotrino-vault service`). Antes la TUI llamaba a `systemctl --user` a pelo, así que en
|
|
7
|
+
* Windows y macOS el botón de arrancar solo sabía fallar.
|
|
8
|
+
*
|
|
9
|
+
* Linux systemd --user si la unidad está instalada (el .deb y el tarball la ponen);
|
|
10
|
+
* si no, el daemon suelto en segundo plano.
|
|
11
|
+
* Windows el daemon suelto, sin ventana. Al iniciar sesión: una entrada en
|
|
12
|
+
* HKCU\…\Run que lo lanza con `conhost --headless` (sin consola a la vista).
|
|
13
|
+
* macOS un LaunchAgent del usuario (`~/Library/LaunchAgents/com.dotrino.vault.plist`)
|
|
14
|
+
* que lo levanta al entrar y lo relanza si se cae; sin él, el daemon suelto.
|
|
15
|
+
*
|
|
16
|
+
* Todo es DEL USUARIO, sin permisos de administrador: es lo que deja que se actualice solo
|
|
17
|
+
* (CONVENCIONES §15).
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'node:fs'
|
|
20
|
+
import os from 'node:os'
|
|
21
|
+
import path from 'node:path'
|
|
22
|
+
import { execFile, spawn } from 'node:child_process'
|
|
23
|
+
import { fileURLToPath } from 'node:url'
|
|
24
|
+
import { dataDir, ensureDir } from './paths.js'
|
|
25
|
+
import { ipcRead } from './ipc.js'
|
|
26
|
+
|
|
27
|
+
const run = (cmd, args, timeout = 15000) => new Promise((resolve) => {
|
|
28
|
+
execFile(cmd, args, { timeout, windowsHide: true }, (err, so, se) => {
|
|
29
|
+
resolve({ ok: !err, out: String(so || ''), err: err ? (String(se || '').trim() || err.message) : '' })
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Cómo se lanza el daemon desde este mismo proceso. En el binario autosuficiente es el
|
|
35
|
+
* propio ejecutable sin argumentos; desde npm, `node bin/dotrino-vaultd.js`.
|
|
36
|
+
*/
|
|
37
|
+
export function daemonCommand () {
|
|
38
|
+
if (globalThis.__DOTRINO_SEA__) return { cmd: process.execPath, args: [] }
|
|
39
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
40
|
+
return { cmd: process.execPath, args: [path.join(here, '..', 'bin', 'dotrino-vaultd.js')] }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Dónde escribe el daemon suelto lo que diría por consola. */
|
|
44
|
+
export const daemonLogFile = () => path.join(dataDir(), 'daemon.log')
|
|
45
|
+
|
|
46
|
+
/** Lanza el daemon suelto, en segundo plano y sin ventana, con su salida a `daemon.log`. */
|
|
47
|
+
export function spawnDaemon () {
|
|
48
|
+
const { cmd, args } = daemonCommand()
|
|
49
|
+
ensureDir(dataDir())
|
|
50
|
+
const fd = fs.openSync(daemonLogFile(), 'a', 0o600)
|
|
51
|
+
try {
|
|
52
|
+
const child = spawn(cmd, args, { detached: true, windowsHide: true, stdio: ['ignore', fd, fd] })
|
|
53
|
+
child.unref()
|
|
54
|
+
return { ok: true, err: '', pid: child.pid }
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return { ok: false, err: e.message }
|
|
57
|
+
} finally { fs.closeSync(fd) }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Para el daemon suelto que esté corriendo (el de `spawnDaemon`), y espera a que se vaya.
|
|
62
|
+
* Hace falta antes de dárselo a un supervisor: el candado del dir de datos solo deja uno,
|
|
63
|
+
* y el del supervisor saldría con error y se relanzaría en bucle.
|
|
64
|
+
*/
|
|
65
|
+
async function stopLooseDaemon () {
|
|
66
|
+
const pid = ipcRead(path.join(dataDir(), 'state.json'), null)?.pid
|
|
67
|
+
if (!pidAlive(pid)) return
|
|
68
|
+
try { process.kill(pid, 'SIGTERM') } catch (_) {}
|
|
69
|
+
await waitGone(pid)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const pidAlive = (pid) => { try { return !!pid && (process.kill(pid, 0) || true) } catch { return false } }
|
|
73
|
+
|
|
74
|
+
/** Espera a que el proceso se vaya (y suelte el candado del dir de datos). */
|
|
75
|
+
async function waitGone (pid) {
|
|
76
|
+
for (let n = 0; n < 40 && pidAlive(pid); n++) await new Promise((r) => setTimeout(r, 250))
|
|
77
|
+
if (pidAlive(pid)) throw new Error(`the running vault (pid ${pid}) did not stop`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ------------------------------- Linux ------------------------------------
|
|
81
|
+
|
|
82
|
+
async function hasSystemdUnit () {
|
|
83
|
+
// Con una carpeta de datos propia, la unidad (que tiene la suya) arrancaría OTRA bóveda.
|
|
84
|
+
if (process.platform !== 'linux' || process.env.DOTRINO_VAULT_DIR) return false
|
|
85
|
+
return (await run('systemctl', ['--user', 'cat', 'dotrino-vault'], 5000)).ok
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ------------------------------- macOS ------------------------------------
|
|
89
|
+
|
|
90
|
+
const LABEL = 'com.dotrino.vault'
|
|
91
|
+
export const launchAgentFile = () => path.join(os.homedir(), 'Library', 'LaunchAgents', LABEL + '.plist')
|
|
92
|
+
const guiDomain = () => `gui/${process.getuid?.() ?? 501}`
|
|
93
|
+
|
|
94
|
+
const xml = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
95
|
+
|
|
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
|
+
*/
|
|
101
|
+
export function launchAgentPlist ({ cmd, args }, log = daemonLogFile()) {
|
|
102
|
+
const argv = [cmd, ...args].map((a) => ` <string>${xml(a)}</string>`).join('\n')
|
|
103
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
104
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
105
|
+
<plist version="1.0">
|
|
106
|
+
<dict>
|
|
107
|
+
<key>Label</key>
|
|
108
|
+
<string>${LABEL}</string>
|
|
109
|
+
<key>ProgramArguments</key>
|
|
110
|
+
<array>
|
|
111
|
+
${argv}
|
|
112
|
+
</array>
|
|
113
|
+
<key>RunAtLoad</key>
|
|
114
|
+
<true/>
|
|
115
|
+
<key>KeepAlive</key>
|
|
116
|
+
<true/>
|
|
117
|
+
<key>StandardOutPath</key>
|
|
118
|
+
<string>${xml(log)}</string>
|
|
119
|
+
<key>StandardErrorPath</key>
|
|
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. -->
|
|
124
|
+
<key>ProcessType</key>
|
|
125
|
+
<string>Standard</string>
|
|
126
|
+
</dict>
|
|
127
|
+
</plist>
|
|
128
|
+
`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ------------------------------- Windows ----------------------------------
|
|
132
|
+
|
|
133
|
+
const RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
|
|
134
|
+
const RUN_VALUE = 'DotrinoVault'
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* La línea que se guarda en `Run`. `conhost --headless` (Windows 10 1809 en adelante) corre
|
|
138
|
+
* una aplicación de consola sin abrirle ventana: sin eso, el daemon sacaría una consola
|
|
139
|
+
* negra cada vez que entras.
|
|
140
|
+
*/
|
|
141
|
+
export function windowsRunLine ({ cmd, args }) {
|
|
142
|
+
const q = (s) => `"${s}"`
|
|
143
|
+
// Con la ventana instalada al lado, ella lanza el daemon sin consola (`--start-daemon`):
|
|
144
|
+
// es una aplicación de ventanas y no depende de `conhost`.
|
|
145
|
+
const gui = path.join(path.dirname(cmd), 'dotrino-vault-desktop.exe')
|
|
146
|
+
if (globalThis.__DOTRINO_SEA__ && fs.existsSync(gui)) return `${q(gui)} --start-daemon`
|
|
147
|
+
return ['conhost.exe', '--headless', q(cmd), ...args.map(q)].join(' ')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ------------------------------- común ------------------------------------
|
|
151
|
+
|
|
152
|
+
/** ¿Arranca solo al iniciar sesión? `null` = no se sabe en este sistema. */
|
|
153
|
+
export async function autostartStatus () {
|
|
154
|
+
if (process.platform === 'linux') {
|
|
155
|
+
if (!(await hasSystemdUnit())) return { on: false, how: 'none' }
|
|
156
|
+
const r = await run('systemctl', ['--user', 'is-enabled', 'dotrino-vault'], 5000)
|
|
157
|
+
return { on: r.out.trim() === 'enabled', how: 'systemd' }
|
|
158
|
+
}
|
|
159
|
+
if (process.platform === 'darwin') return { on: fs.existsSync(launchAgentFile()), how: 'launchd' }
|
|
160
|
+
if (process.platform === 'win32') {
|
|
161
|
+
const r = await run('reg', ['query', RUN_KEY, '/v', RUN_VALUE], 5000)
|
|
162
|
+
return { on: r.ok, how: 'run-key' }
|
|
163
|
+
}
|
|
164
|
+
return { on: null, how: 'unknown' }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Deja el daemon arrancando al iniciar sesión (y lo arranca ya, si no está). */
|
|
168
|
+
export async function enableAutostart () {
|
|
169
|
+
const dc = daemonCommand()
|
|
170
|
+
if (process.platform === 'linux') {
|
|
171
|
+
if (!(await hasSystemdUnit())) return { ok: false, err: 'no systemd user unit installed (install the .deb, .rpm or the tarball)' }
|
|
172
|
+
return run('systemctl', ['--user', 'enable', '--now', 'dotrino-vault'])
|
|
173
|
+
}
|
|
174
|
+
if (process.platform === 'darwin') {
|
|
175
|
+
const file = launchAgentFile()
|
|
176
|
+
ensureDir(path.dirname(file))
|
|
177
|
+
fs.writeFileSync(file, launchAgentPlist(dc), { mode: 0o644 })
|
|
178
|
+
await run('launchctl', ['bootout', `${guiDomain()}/${LABEL}`], 5000)
|
|
179
|
+
// Si ya hay un daemon suelto corriendo, el agente lo encontraría ocupado y se
|
|
180
|
+
// relanzaría en bucle: el candado del dir de datos solo deja uno. Se para y lo levanta
|
|
181
|
+
// el agente, que es quien lo cuidará desde ahora.
|
|
182
|
+
try { await stopLooseDaemon() } catch (e) { return { ok: false, err: e.message } }
|
|
183
|
+
return run('launchctl', ['bootstrap', guiDomain(), file])
|
|
184
|
+
}
|
|
185
|
+
if (process.platform === 'win32') {
|
|
186
|
+
return run('reg', ['add', RUN_KEY, '/v', RUN_VALUE, '/t', 'REG_SZ', '/d', windowsRunLine(dc), '/f'])
|
|
187
|
+
}
|
|
188
|
+
return { ok: false, err: `unsupported platform: ${process.platform}` }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Deja de arrancarlo al iniciar sesión. NO lo para: eso es otra decisión. */
|
|
192
|
+
export async function disableAutostart () {
|
|
193
|
+
if (process.platform === 'linux') return run('systemctl', ['--user', 'disable', 'dotrino-vault'])
|
|
194
|
+
if (process.platform === 'darwin') {
|
|
195
|
+
const file = launchAgentFile()
|
|
196
|
+
if (!fs.existsSync(file)) return { ok: true, err: '' }
|
|
197
|
+
// `bootout` también lo PARA. Se vuelve a lanzar suelto para no apagarle la bóveda a
|
|
198
|
+
// quien solo quería que no arrancara sola la próxima vez.
|
|
199
|
+
const wasRunning = (await run('launchctl', ['print', `${guiDomain()}/${LABEL}`], 5000)).ok
|
|
200
|
+
const pid = ipcRead(path.join(dataDir(), 'state.json'), null)?.pid
|
|
201
|
+
await run('launchctl', ['bootout', `${guiDomain()}/${LABEL}`], 5000)
|
|
202
|
+
fs.rmSync(file, { force: true })
|
|
203
|
+
if (!wasRunning) return { ok: true, err: '' }
|
|
204
|
+
// El suelto entra cuando el del agente ya soltó el candado; antes chocaría con él.
|
|
205
|
+
try { await waitGone(pid) } catch (e) { return { ok: false, err: e.message } }
|
|
206
|
+
return spawnDaemon()
|
|
207
|
+
}
|
|
208
|
+
if (process.platform === 'win32') {
|
|
209
|
+
const r = await run('reg', ['delete', RUN_KEY, '/v', RUN_VALUE, '/f'])
|
|
210
|
+
// Que no existiera no es un fallo: el resultado es el que se pedía.
|
|
211
|
+
return r.ok || /unable to find|no se puede encontrar|not find/i.test(r.err) ? { ok: true, err: '' } : r
|
|
212
|
+
}
|
|
213
|
+
return { ok: false, err: `unsupported platform: ${process.platform}` }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Arranca el daemon por el camino de este sistema. */
|
|
217
|
+
export async function startDaemonService () {
|
|
218
|
+
if (await hasSystemdUnit()) return run('systemctl', ['--user', 'start', 'dotrino-vault'], 8000)
|
|
219
|
+
if (process.platform === 'darwin' && fs.existsSync(launchAgentFile())) {
|
|
220
|
+
const r = await run('launchctl', ['kickstart', `${guiDomain()}/${LABEL}`], 8000)
|
|
221
|
+
if (r.ok) return r
|
|
222
|
+
// El agente está escrito pero no cargado (se borró a mano de launchctl): se carga.
|
|
223
|
+
return run('launchctl', ['bootstrap', guiDomain(), launchAgentFile()], 8000)
|
|
224
|
+
}
|
|
225
|
+
return spawnDaemon()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Reinicia el daemon (tras actualizar). Con systemd o launchd lo hace el supervisor; suelto,
|
|
230
|
+
* se le pide que se vaya y se lanza el nuevo cuando ya no está — dos a la vez no pueden,
|
|
231
|
+
* el candado del dir de datos lo impide.
|
|
232
|
+
*/
|
|
233
|
+
export async function restartDaemonService ({ pid, alive } = {}) {
|
|
234
|
+
if (await hasSystemdUnit()) {
|
|
235
|
+
const r = await run('systemctl', ['--user', 'restart', 'dotrino-vault'], 20000)
|
|
236
|
+
if (!r.ok) throw new Error(r.err)
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
if (process.platform === 'darwin' && fs.existsSync(launchAgentFile())) {
|
|
240
|
+
const r = await run('launchctl', ['kickstart', '-k', `${guiDomain()}/${LABEL}`], 20000)
|
|
241
|
+
if (!r.ok) throw new Error(r.err)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
if (pid) {
|
|
245
|
+
try { process.kill(pid, 'SIGTERM') } catch (_) {}
|
|
246
|
+
for (let n = 0; n < 40 && alive?.(); n++) await new Promise((r) => setTimeout(r, 250))
|
|
247
|
+
if (alive?.()) throw new Error('the old daemon did not stop')
|
|
248
|
+
}
|
|
249
|
+
const r = spawnDaemon()
|
|
250
|
+
if (!r.ok) throw new Error(r.err)
|
|
251
|
+
}
|