@dotrino/vaultd 0.156.0 → 0.157.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 +51 -14
- package/bin/sea-entry.js +4 -0
- package/package.json +1 -1
- package/src/ctl.js +39 -1
- package/src/service.js +247 -0
- package/src/tui/app.js +319 -127
- package/src/tui/gui.js +214 -0
- package/src/tui/i18n.js +25 -6
- package/src/tui/term.js +7 -2
- package/src/update.js +5 -1
package/README.md
CHANGED
|
@@ -128,10 +128,43 @@ 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
|
+
> Ni el instalador de Windows ni la app de macOS llevan la firma de un desarrollador
|
|
163
|
+
> registrado: Windows enseña el aviso de SmartScreen («editor desconocido») y macOS el de
|
|
164
|
+
> Gatekeeper la primera vez (clic derecho → Abrir). Lo que prueba de dónde salieron es la
|
|
165
|
+
> atestación de sigstore: `gh attestation verify <archivo> --repo imdotrino/dotrino-vault`.
|
|
166
|
+
> En Windows y macOS la bóveda todavía **no se actualiza sola**: se instala la versión nueva
|
|
167
|
+
> encima.
|
|
135
168
|
|
|
136
169
|
**Ubuntu / Debian — `.deb`:** descarga el `.deb` (versionado) desde
|
|
137
170
|
[Releases](https://github.com/imdotrino/dotrino-vault/releases/latest) y haz doble
|
|
@@ -196,7 +229,7 @@ npx -y @dotrino/vaultd # Node ≥ 20
|
|
|
196
229
|
npx -y @dotrino/vaultd --tui # la bóveda y su pantalla de control en la misma ventana
|
|
197
230
|
```
|
|
198
231
|
|
|
199
|
-
|
|
232
|
+
Sirve en cualquier sistema (y es la vía en ARM fuera de Docker). Arranca en primer
|
|
200
233
|
plano: la bóveda vive mientras dejes esa ventana abierta. Los comandos de control se
|
|
201
234
|
anteponen con `npx -p @dotrino/vaultd` (por ejemplo, `npx -p @dotrino/vaultd
|
|
202
235
|
dotrino-vault pair`). Si no tienes Node, el instalador del ecosistema lo baja en tu
|
|
@@ -228,9 +261,9 @@ recupera.
|
|
|
228
261
|
|
|
229
262
|
> Sin firma de código: tu sistema puede advertir que el binario no está firmado. Es
|
|
230
263
|
> autohospedado y de código abierto; en Linux solo necesita permiso de ejecución (el
|
|
231
|
-
> instalador lo da).
|
|
232
|
-
>
|
|
233
|
-
>
|
|
264
|
+
> instalador lo da). El daemon de Windows (`packaging/build-win.sh`) se construye desde
|
|
265
|
+
> Linux y va dentro del instalador de Windows; el de macOS (`packaging/build-mac.sh`) se
|
|
266
|
+
> construye en un Mac y va dentro de la app.
|
|
234
267
|
|
|
235
268
|
**Dónde vive todo y cómo se para.** Tus datos —llave incluida— viven en
|
|
236
269
|
`~/.local/share/dotrino/vault` (Linux y macOS), `%LOCALAPPDATA%\Dotrino\vault`
|
|
@@ -848,8 +881,10 @@ ed25519 en formato OpenSSH (sin frase: la bóveda es el candado) y RSA/P-256 en
|
|
|
848
881
|
## Alcance
|
|
849
882
|
|
|
850
883
|
- **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
|
-
|
|
884
|
+
servicio `systemd --user` (binario único Node SEA: `.deb`, `.rpm` y tarball); en
|
|
885
|
+
**Windows** con un instalador por usuario y en **macOS** como `Dotrino Vault.app`, los
|
|
886
|
+
dos con arranque al iniciar sesión; en los tres, con **una ventana** que tiene las
|
|
887
|
+
pantallas de la TUI (`desktop/`); y en cualquier sistema, con `npx` o **Docker**
|
|
853
888
|
(imagen amd64/arm64 en GHCR). Identidad **cifrada en reposo y ligada a esta
|
|
854
889
|
máquina**. Emparejamiento endurecido con los dos caminos de vinculación, acta del
|
|
855
890
|
perfil (miembros, capacidades y `cn` de servicio), firma delegada, renovación
|
|
@@ -858,9 +893,9 @@ ed25519 en formato OpenSSH (sin frase: la bóveda es el candado) y RSA/P-256 en
|
|
|
858
893
|
actividad. Contraseña opcional por perfil, **para editarlo** (verificador PBKDF2).
|
|
859
894
|
- **v2:** cifrado en reposo **con la contraseña del perfil** (hoy la clave sale solo de
|
|
860
895
|
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`).
|
|
896
|
+
al TPM; cifrar también los secretos y el store; que la bóveda de Windows y macOS se
|
|
897
|
+
actualice sola, como la de Linux; firma de Apple y de Microsoft; firma de documentos
|
|
898
|
+
con sellado de tiempo (`dotrino-signer`).
|
|
864
899
|
|
|
865
900
|
## Estructura
|
|
866
901
|
|
|
@@ -870,7 +905,9 @@ ed25519 en formato OpenSSH (sin frase: la bóveda es el candado) y RSA/P-256 en
|
|
|
870
905
|
- `src/daemon.js` — modo servicio: `state.json`, control por archivos + señales, apagado limpio.
|
|
871
906
|
- `src/ctl.js` — CLI de control (habla con el daemon por archivos + señales, sin socket).
|
|
872
907
|
- `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).
|
|
908
|
+
- `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).
|
|
909
|
+
- `src/service.js` — arrancar, reiniciar y arrancar al iniciar sesión en Linux, Windows y macOS.
|
|
910
|
+
- `desktop/` — la ventana (Rust + `iced`): pinta lo que le manda `gui.js`.
|
|
874
911
|
- `src/transport.js` — conexión headless al proxy + `identify` firmado (con el acta).
|
|
875
912
|
- `src/threadStore.js` — el store del usuario: hilos, "recientes" y perfil (`threads.json`).
|
|
876
913
|
- `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.157.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Certificador personal de Dotrino: daemon headless que custodia la clave maestra y delega capacidades a tus dispositivos por el proxy. Tu CA propia.",
|
|
6
6
|
"bin": {
|
package/src/ctl.js
CHANGED
|
@@ -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'
|
|
@@ -2240,6 +2240,36 @@ function cmdLogs () {
|
|
|
2240
2240
|
}
|
|
2241
2241
|
|
|
2242
2242
|
// Import dinámico: así `dotrino-vault status` (el caso común) no carga la TUI.
|
|
2243
|
+
/**
|
|
2244
|
+
* `service start | autostart [on|off]`: arrancar el daemon y dejarlo arrancando al iniciar
|
|
2245
|
+
* sesión, en los tres sistemas (`src/service.js`). En Linux es la unidad de systemd; en
|
|
2246
|
+
* Windows una entrada de inicio de sesión; en macOS un LaunchAgent.
|
|
2247
|
+
*/
|
|
2248
|
+
async function cmdService (rest) {
|
|
2249
|
+
const sv = await import('./service.js')
|
|
2250
|
+
const [sub, val] = rest
|
|
2251
|
+
if (sub === 'start') {
|
|
2252
|
+
// `readState()` sale si no hay state.json, y sin state.json es justo cuando hay que arrancar.
|
|
2253
|
+
const s = ipcRead(stateFile, null)
|
|
2254
|
+
if (s && alive(s.pid)) { console.log('ya está corriendo (pid ' + s.pid + ')'); return }
|
|
2255
|
+
const r = await sv.startDaemonService()
|
|
2256
|
+
if (!r.ok) { console.error('no se pudo arrancar: ' + r.err); process.exit(1) }
|
|
2257
|
+
console.log('arrancado · registro: ' + sv.daemonLogFile())
|
|
2258
|
+
return
|
|
2259
|
+
}
|
|
2260
|
+
if (sub === 'autostart') {
|
|
2261
|
+
if (val === 'on' || val === 'off') {
|
|
2262
|
+
const r = val === 'on' ? await sv.enableAutostart() : await sv.disableAutostart()
|
|
2263
|
+
if (!r.ok) { console.error('no se pudo: ' + r.err); process.exit(1) }
|
|
2264
|
+
} else if (val !== undefined) { console.error('uso: dotrino-vault service autostart [on|off]'); process.exit(2) }
|
|
2265
|
+
const st = await sv.autostartStatus()
|
|
2266
|
+
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})` : ''))
|
|
2267
|
+
return
|
|
2268
|
+
}
|
|
2269
|
+
console.error('uso: dotrino-vault service start | autostart [on|off]')
|
|
2270
|
+
process.exit(2)
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2243
2273
|
async function cmdTui () {
|
|
2244
2274
|
if (!process.stdout.isTTY) { console.error('la TUI necesita un terminal interactivo (TTY).'); process.exit(2) }
|
|
2245
2275
|
const { runTui } = await import('./tui/app.js')
|
|
@@ -2250,6 +2280,10 @@ function help () {
|
|
|
2250
2280
|
console.log(`dotrino-vault — control del certificador personal
|
|
2251
2281
|
|
|
2252
2282
|
tui interfaz de terminal a pantalla completa (bóvedas, pares, secretos)
|
|
2283
|
+
service start arranca el servicio en segundo plano (sin ventana)
|
|
2284
|
+
service autostart [on|off]
|
|
2285
|
+
que arranque solo al iniciar sesión (systemd en Linux, una entrada
|
|
2286
|
+
de inicio en Windows, un LaunchAgent en macOS) · sin valor, dice cómo está
|
|
2253
2287
|
status | info estado del servicio + fingerprint + el id de cada perfil
|
|
2254
2288
|
update [--check] trae la versión nueva (verificando la firma) y espera a que el
|
|
2255
2289
|
servicio la arranque · --check solo mira · --no-wait no espera
|
|
@@ -2485,6 +2519,10 @@ export async function runCtl (argv) {
|
|
|
2485
2519
|
if (rest.includes('--help') || rest.includes('-h')) return help()
|
|
2486
2520
|
switch (cmd) {
|
|
2487
2521
|
case 'tui': return cmdTui()
|
|
2522
|
+
// El canal de la ventana de escritorio (`desktop/`). No es para personas: habla JSON por
|
|
2523
|
+
// stdin/stdout. Ver `src/tui/gui.js`.
|
|
2524
|
+
case 'gui-bridge': { await (await import('./tui/gui.js')).runGuiBridge(); process.exit(0) }
|
|
2525
|
+
case 'service': return cmdService(rest)
|
|
2488
2526
|
case 'profile': return cmdProfile(rest)
|
|
2489
2527
|
case 'unlock': return cmdUnlock(rest)
|
|
2490
2528
|
case 'lock': return cmdLock()
|
package/src/service.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
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
|
+
/** El LaunchAgent: lo arranca al entrar y lo relanza si se cae (no si se para a propósito). */
|
|
97
|
+
export function launchAgentPlist ({ cmd, args }, log = daemonLogFile()) {
|
|
98
|
+
const argv = [cmd, ...args].map((a) => ` <string>${xml(a)}</string>`).join('\n')
|
|
99
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
100
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
101
|
+
<plist version="1.0">
|
|
102
|
+
<dict>
|
|
103
|
+
<key>Label</key>
|
|
104
|
+
<string>${LABEL}</string>
|
|
105
|
+
<key>ProgramArguments</key>
|
|
106
|
+
<array>
|
|
107
|
+
${argv}
|
|
108
|
+
</array>
|
|
109
|
+
<key>RunAtLoad</key>
|
|
110
|
+
<true/>
|
|
111
|
+
<key>KeepAlive</key>
|
|
112
|
+
<dict>
|
|
113
|
+
<key>SuccessfulExit</key>
|
|
114
|
+
<false/>
|
|
115
|
+
</dict>
|
|
116
|
+
<key>StandardOutPath</key>
|
|
117
|
+
<string>${xml(log)}</string>
|
|
118
|
+
<key>StandardErrorPath</key>
|
|
119
|
+
<string>${xml(log)}</string>
|
|
120
|
+
<key>ProcessType</key>
|
|
121
|
+
<string>Background</string>
|
|
122
|
+
</dict>
|
|
123
|
+
</plist>
|
|
124
|
+
`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ------------------------------- Windows ----------------------------------
|
|
128
|
+
|
|
129
|
+
const RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
|
|
130
|
+
const RUN_VALUE = 'DotrinoVault'
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* La línea que se guarda en `Run`. `conhost --headless` (Windows 10 1809 en adelante) corre
|
|
134
|
+
* una aplicación de consola sin abrirle ventana: sin eso, el daemon sacaría una consola
|
|
135
|
+
* negra cada vez que entras.
|
|
136
|
+
*/
|
|
137
|
+
export function windowsRunLine ({ cmd, args }) {
|
|
138
|
+
const q = (s) => `"${s}"`
|
|
139
|
+
// Con la ventana instalada al lado, ella lanza el daemon sin consola (`--start-daemon`):
|
|
140
|
+
// es una aplicación de ventanas y no depende de `conhost`.
|
|
141
|
+
const gui = path.join(path.dirname(cmd), 'dotrino-vault-desktop.exe')
|
|
142
|
+
if (globalThis.__DOTRINO_SEA__ && fs.existsSync(gui)) return `${q(gui)} --start-daemon`
|
|
143
|
+
return ['conhost.exe', '--headless', q(cmd), ...args.map(q)].join(' ')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ------------------------------- común ------------------------------------
|
|
147
|
+
|
|
148
|
+
/** ¿Arranca solo al iniciar sesión? `null` = no se sabe en este sistema. */
|
|
149
|
+
export async function autostartStatus () {
|
|
150
|
+
if (process.platform === 'linux') {
|
|
151
|
+
if (!(await hasSystemdUnit())) return { on: false, how: 'none' }
|
|
152
|
+
const r = await run('systemctl', ['--user', 'is-enabled', 'dotrino-vault'], 5000)
|
|
153
|
+
return { on: r.out.trim() === 'enabled', how: 'systemd' }
|
|
154
|
+
}
|
|
155
|
+
if (process.platform === 'darwin') return { on: fs.existsSync(launchAgentFile()), how: 'launchd' }
|
|
156
|
+
if (process.platform === 'win32') {
|
|
157
|
+
const r = await run('reg', ['query', RUN_KEY, '/v', RUN_VALUE], 5000)
|
|
158
|
+
return { on: r.ok, how: 'run-key' }
|
|
159
|
+
}
|
|
160
|
+
return { on: null, how: 'unknown' }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Deja el daemon arrancando al iniciar sesión (y lo arranca ya, si no está). */
|
|
164
|
+
export async function enableAutostart () {
|
|
165
|
+
const dc = daemonCommand()
|
|
166
|
+
if (process.platform === 'linux') {
|
|
167
|
+
if (!(await hasSystemdUnit())) return { ok: false, err: 'no systemd user unit installed (install the .deb, .rpm or the tarball)' }
|
|
168
|
+
return run('systemctl', ['--user', 'enable', '--now', 'dotrino-vault'])
|
|
169
|
+
}
|
|
170
|
+
if (process.platform === 'darwin') {
|
|
171
|
+
const file = launchAgentFile()
|
|
172
|
+
ensureDir(path.dirname(file))
|
|
173
|
+
fs.writeFileSync(file, launchAgentPlist(dc), { mode: 0o644 })
|
|
174
|
+
await run('launchctl', ['bootout', `${guiDomain()}/${LABEL}`], 5000)
|
|
175
|
+
// Si ya hay un daemon suelto corriendo, el agente lo encontraría ocupado y se
|
|
176
|
+
// relanzaría en bucle: el candado del dir de datos solo deja uno. Se para y lo levanta
|
|
177
|
+
// el agente, que es quien lo cuidará desde ahora.
|
|
178
|
+
try { await stopLooseDaemon() } catch (e) { return { ok: false, err: e.message } }
|
|
179
|
+
return run('launchctl', ['bootstrap', guiDomain(), file])
|
|
180
|
+
}
|
|
181
|
+
if (process.platform === 'win32') {
|
|
182
|
+
return run('reg', ['add', RUN_KEY, '/v', RUN_VALUE, '/t', 'REG_SZ', '/d', windowsRunLine(dc), '/f'])
|
|
183
|
+
}
|
|
184
|
+
return { ok: false, err: `unsupported platform: ${process.platform}` }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Deja de arrancarlo al iniciar sesión. NO lo para: eso es otra decisión. */
|
|
188
|
+
export async function disableAutostart () {
|
|
189
|
+
if (process.platform === 'linux') return run('systemctl', ['--user', 'disable', 'dotrino-vault'])
|
|
190
|
+
if (process.platform === 'darwin') {
|
|
191
|
+
const file = launchAgentFile()
|
|
192
|
+
if (!fs.existsSync(file)) return { ok: true, err: '' }
|
|
193
|
+
// `bootout` también lo PARA. Se vuelve a lanzar suelto para no apagarle la bóveda a
|
|
194
|
+
// quien solo quería que no arrancara sola la próxima vez.
|
|
195
|
+
const wasRunning = (await run('launchctl', ['print', `${guiDomain()}/${LABEL}`], 5000)).ok
|
|
196
|
+
const pid = ipcRead(path.join(dataDir(), 'state.json'), null)?.pid
|
|
197
|
+
await run('launchctl', ['bootout', `${guiDomain()}/${LABEL}`], 5000)
|
|
198
|
+
fs.rmSync(file, { force: true })
|
|
199
|
+
if (!wasRunning) return { ok: true, err: '' }
|
|
200
|
+
// El suelto entra cuando el del agente ya soltó el candado; antes chocaría con él.
|
|
201
|
+
try { await waitGone(pid) } catch (e) { return { ok: false, err: e.message } }
|
|
202
|
+
return spawnDaemon()
|
|
203
|
+
}
|
|
204
|
+
if (process.platform === 'win32') {
|
|
205
|
+
const r = await run('reg', ['delete', RUN_KEY, '/v', RUN_VALUE, '/f'])
|
|
206
|
+
// Que no existiera no es un fallo: el resultado es el que se pedía.
|
|
207
|
+
return r.ok || /unable to find|no se puede encontrar|not find/i.test(r.err) ? { ok: true, err: '' } : r
|
|
208
|
+
}
|
|
209
|
+
return { ok: false, err: `unsupported platform: ${process.platform}` }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Arranca el daemon por el camino de este sistema. */
|
|
213
|
+
export async function startDaemonService () {
|
|
214
|
+
if (await hasSystemdUnit()) return run('systemctl', ['--user', 'start', 'dotrino-vault'], 8000)
|
|
215
|
+
if (process.platform === 'darwin' && fs.existsSync(launchAgentFile())) {
|
|
216
|
+
const r = await run('launchctl', ['kickstart', `${guiDomain()}/${LABEL}`], 8000)
|
|
217
|
+
if (r.ok) return r
|
|
218
|
+
// El agente está escrito pero no cargado (se borró a mano de launchctl): se carga.
|
|
219
|
+
return run('launchctl', ['bootstrap', guiDomain(), launchAgentFile()], 8000)
|
|
220
|
+
}
|
|
221
|
+
return spawnDaemon()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Reinicia el daemon (tras actualizar). Con systemd o launchd lo hace el supervisor; suelto,
|
|
226
|
+
* se le pide que se vaya y se lanza el nuevo cuando ya no está — dos a la vez no pueden,
|
|
227
|
+
* el candado del dir de datos lo impide.
|
|
228
|
+
*/
|
|
229
|
+
export async function restartDaemonService ({ pid, alive } = {}) {
|
|
230
|
+
if (await hasSystemdUnit()) {
|
|
231
|
+
const r = await run('systemctl', ['--user', 'restart', 'dotrino-vault'], 20000)
|
|
232
|
+
if (!r.ok) throw new Error(r.err)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
if (process.platform === 'darwin' && fs.existsSync(launchAgentFile())) {
|
|
236
|
+
const r = await run('launchctl', ['kickstart', '-k', `${guiDomain()}/${LABEL}`], 20000)
|
|
237
|
+
if (!r.ok) throw new Error(r.err)
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
if (pid) {
|
|
241
|
+
try { process.kill(pid, 'SIGTERM') } catch (_) {}
|
|
242
|
+
for (let n = 0; n < 40 && alive?.(); n++) await new Promise((r) => setTimeout(r, 250))
|
|
243
|
+
if (alive?.()) throw new Error('the old daemon did not stop')
|
|
244
|
+
}
|
|
245
|
+
const r = spawnDaemon()
|
|
246
|
+
if (!r.ok) throw new Error(r.err)
|
|
247
|
+
}
|