@dotrino/vaultd 0.6.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 ADDED
@@ -0,0 +1,301 @@
1
+ # Dotrino Vault — tu certificador personal
2
+
3
+ > **Parte del ecosistema [Dotrino](https://dotrino.com).** Tu identidad, en tu
4
+ > máquina, bajo tus reglas — sin anuncios, sin cookies, sin rastreo.
5
+
6
+ `dotrino-vault` es el **certificador personal** del usuario: un **servicio headless**
7
+ que custodia tu **clave maestra** y actúa como tu **propia CA**. En vez de depender
8
+ de las CAs, del "Inicia sesión con Google/Apple" o de verificadores de KYC, **tú
9
+ certificas**: enrolas tus dispositivos, firmas documentos y avalas a otras personas,
10
+ sin pedirle permiso a ningún portero central. La maestra **nunca sale** de tu máquina.
11
+
12
+ ## Modelo: identidad delegada (la maestra se queda en una sola máquina)
13
+
14
+ ```
15
+ PC (vault) ── clave maestra P (NUNCA sale) ────────────────────────────┐
16
+ · genera/custodia P (vía @dotrino/identity) │
17
+ · firma un CERT por dispositivo: "D puede <scope> en nombre de P, │
18
+ hasta <exp>, revocable por <nonce>" │
19
+ · firma datos a pedido de un dispositivo enrolado (devuelve solo la firma)
20
+ ▲ proxy (sendByPubkey + cola offline 24h) │
21
+ │ │
22
+ cel / laptop ── su propia sub-clave D (P nunca la ve) ────────────────┘
23
+ · se enrola escaneando un QR del vault → recibe su cert
24
+ · firma cada acción con D y adjunta el cert; el vault verifica la CADENA D←P
25
+ ```
26
+
27
+ Todo esto **no se reimplementa**: la cripto de delegación vive en
28
+ `@dotrino/identity` (`signDelegation`, `verifyChain`, `makeDeviceKey`), el transporte
29
+ es `@dotrino/proxy-client`. Este repo solo **orquesta**.
30
+
31
+ ## Instalación (Linux)
32
+
33
+ **Ubuntu / Debian — `.deb`** (lo más simple): descarga el `.deb` (versionado) desde
34
+ [Releases](https://github.com/imdotrino/dotrino-vault/releases/latest) y haz doble
35
+ clic, o en la terminal:
36
+
37
+ ```sh
38
+ sudo apt install ./dotrino-vault_*.deb
39
+ ```
40
+
41
+ **Otro Linux — tarball:** descarga el binario autosuficiente y ejecuta el instalador:
42
+
43
+ ```sh
44
+ tar xzf dotrino-vault-*-linux-x64.tar.gz
45
+ cd dotrino-vault-*-linux-x64
46
+ sh install.sh
47
+ ```
48
+
49
+ El `.deb` deja los binarios en `/usr/bin`, instala la unidad `systemd --user` y la
50
+ habilita; el tarball hace lo equivalente en tu `$HOME`. Ambos: nada de Node ni
51
+ dependencias, y el servicio arranca solo.
52
+
53
+ El binario **trae Node embebido**: no necesitas instalar Node ni dependencias de npm. Lo
54
+ único que espera del sistema es la biblioteca `libatomic1`, que casi todas las distribuciones
55
+ traen puesta; el `.deb` la declara y la instala sola si falta. Con el tarball, en un sistema
56
+ muy pelado (un contenedor mínimo, por ejemplo), instálala tú:
57
+ `sudo apt install libatomic1`. El instalador lo
58
+ deja como **servicio systemd `--user`** que arranca solo (también en el boot, vía
59
+ `linger`). En el primer arranque genera tu identidad y se conecta al proxy. **Sin
60
+ contraseña, sin abrir puertos** (el vault marca hacia afuera).
61
+
62
+ > Sin firma de código: tu sistema puede advertir que el binario no está firmado. Es
63
+ > autohospedado y de código abierto; en Linux solo necesita permiso de ejecución (el
64
+ > instalador lo da). macOS y Windows llegan en v2.
65
+
66
+ ### CLI de control
67
+
68
+ ```sh
69
+ dotrino-vault tui # interfaz de terminal a pantalla completa (ver abajo)
70
+ dotrino-vault status # estado del servicio + fingerprint
71
+ dotrino-vault pair # inicia un emparejamiento (muestra el código y espera al dispositivo)
72
+ dotrino-vault pending # muestra el dispositivo pendiente + su código a comparar
73
+ dotrino-vault approve <deviceId> # aprueba un dispositivo (tras comparar el código en ambas pantallas)
74
+ dotrino-vault reject <deviceId> # rechaza un dispositivo pendiente
75
+ dotrino-vault devices # lista dispositivos enrolados / revocados
76
+ dotrino-vault revoke <nonce> # revoca un dispositivo (le ordena autoborrarse)
77
+ dotrino-vault pair --service <ns> # empareja un SERVICIO (proxy, geo…) con acceso SOLO a sus secretos
78
+ dotrino-vault secret set <ns> <CLAVE> <valor> # guarda un secreto para ese servicio
79
+ dotrino-vault secret rm <ns> <CLAVE> # borra un secreto
80
+ dotrino-vault secret list # nombres de secretos (nunca valores)
81
+ dotrino-vault logs # últimos logs del servicio
82
+ ```
83
+
84
+ ### Interfaz de terminal (TUI)
85
+
86
+ Todo lo anterior (bóvedas, dispositivos y secretos) también se maneja desde una
87
+ **interfaz de terminal a pantalla completa**, sin memorizar subcomandos:
88
+
89
+ ```sh
90
+ dotrino-vault tui # binario instalado
91
+ node bin/dotrino-vault-tui.js # en desarrollo (o: npm run tui)
92
+ ```
93
+
94
+ Como la CLI, la TUI **no abre la identidad ni la red**: le habla al daemon por el
95
+ mismo canal de archivos + señales. El daemon debe estar corriendo; si no, la TUI
96
+ ofrece arrancarlo. No agrega dependencias (se dibuja con ANSI y lee el teclado en
97
+ raw mode).
98
+
99
+ **Navegación en dos niveles**, para que siempre sea explícito de qué bóveda son
100
+ los dispositivos/variables que estás viendo:
101
+
102
+ 1. **Bóvedas** es la pantalla de entrada: lista tus perfiles (`↑↓` mover, `Enter`
103
+ **entrar** a uno — lo activa si no lo estaba). Ahí también creas una bóveda
104
+ nueva, renombras, borras y pones/quitas/usas la contraseña (candado).
105
+ 2. Al entrar caes en sus **pestañas horizontales**, que cambias con `←→`:
106
+ - **Dispositivos (pares):** verlos, **emparejar** uno nuevo (muestra el QR y la
107
+ URL, y espera a que se conecte), **aprobar** con el código que muestra el
108
+ dispositivo, **rechazar** y **revocar**.
109
+ - **Scopes y variables (secretos):** ver los scopes y sus variables (nunca los
110
+ valores), **agregar** una variable (con su scope) y **quitar** una variable o
111
+ un scope entero.
112
+
113
+ `Esc` desde las pestañas vuelve a la lista de bóvedas (para entrar a otra); `q`
114
+ sale desde cualquier pantalla. Las teclas de cada acción se listan en la barra
115
+ inferior.
116
+
117
+ ### Varios perfiles en el mismo PC
118
+
119
+ Puedes tener varias identidades tuyas en la misma máquina (p. ej. personal y
120
+ trabajo). Cada perfil es **una identidad distinta**: su propia clave, sus propios
121
+ dispositivos, sus propios datos y secretos — nada se cruza entre ellos. **Todos
122
+ atienden a la vez**: el perfil «activo» solo decide a cuál va un comando cuando no
123
+ lo dices con `--profile`, no apaga a los demás.
124
+
125
+ ```sh
126
+ dotrino-vault profile ls # lista los perfiles (* = el activo)
127
+ dotrino-vault profile add Trabajo # crea un perfil (identidad nueva, vacía)
128
+ dotrino-vault profile use Trabajo # elige el activo
129
+ dotrino-vault profile rename <nombre> # renombra
130
+ dotrino-vault profile rm Trabajo # BORRA el perfil y su identidad (irreversible)
131
+
132
+ dotrino-vault pair --profile Trabajo # cualquier comando acepta --profile
133
+ dotrino-vault devices --profile personal
134
+ ```
135
+
136
+ Si ya usabas el vault antes de esto, tu identidad de siempre se convierte sola en
137
+ el primer perfil («Perfil 1»): la misma clave, los mismos dispositivos, nada que
138
+ volver a emparejar.
139
+
140
+ ### Contraseña del perfil (opcional)
141
+
142
+ Cada perfil puede llevar contraseña. **Solo se pide para EDITAR el perfil** (cambiar
143
+ tu nombre, avatar o datos): tus dispositivos siguen firmando, leyendo y guardando
144
+ aunque el perfil esté bloqueado — así un reinicio del PC nunca deja tus apps
145
+ muertas esperando a que alguien teclee algo.
146
+
147
+ ```sh
148
+ dotrino-vault profile password # pone o cambia la contraseña (te la pregunta)
149
+ dotrino-vault profile password rm # la quita
150
+ dotrino-vault unlock # desbloquea para poder editar
151
+ dotrino-vault lock # vuelve a bloquear
152
+ ```
153
+
154
+ El perfil se vuelve a bloquear al reiniciar el servicio. La contraseña **no se
155
+ guarda**: solo un verificador con sal (PBKDF2), igual que el candado del navegador.
156
+ Para que quede claro qué protege y qué no: evita que otro que se siente en tu
157
+ máquina —o un dispositivo tuyo comprometido— te reescriba el perfil; **no** cifra la
158
+ clave en el disco (eso es el cifrado en reposo, ver *Alcance*).
159
+
160
+ **Emparejamiento endurecido** (ver [`docs/pairing-protocol.md`](./docs/pairing-protocol.md)):
161
+ el dispositivo prueba posesión de su llave firmando el enrolamiento, y la maestra
162
+ solo firma el certificado **después** de que compares un código de 6 dígitos (SAS)
163
+ entre las dos pantallas y corras `approve`. Un código robado ya no alcanza para
164
+ entrar; la revocación de un dispositivo le ordena **autoborrarse** (con firma de la
165
+ maestra, no por un mensaje cualquiera).
166
+
167
+ El servicio se gestiona con systemd `--user`
168
+ (`systemctl --user {start,stop,restart} dotrino-vault`). Tus datos —clave maestra
169
+ incluida— viven en `~/.local/share/dotrino/vault` (permisos `0600`/`0700`), con un
170
+ subdirectorio `p/<id>/` por perfil.
171
+
172
+ ## Desarrollo
173
+
174
+ ```sh
175
+ npm install
176
+ node bin/dotrino-vaultd.js # arranca el daemon (modo servicio)
177
+ node bin/dotrino-vaultd.js --pair # arranca + imprime un QR de emparejamiento
178
+ bash packaging/build.sh # compila el binario único (dist/)
179
+ ```
180
+
181
+ ### Enrolar y usar desde un dispositivo (Node, para testing)
182
+
183
+ ```js
184
+ import { enroll, requestSign } from 'dotrino-vault/src/client.js'
185
+
186
+ // 1) escaneas el QR del vault → obtienes { iss, proxy, token }
187
+ const { device, cert, iss } = await enroll({ qr }) // GUARDA device (privada) + cert
188
+
189
+ // 2) le pides a la maestra que firme algo (la maestra nunca sale del vault)
190
+ const { signature } = await requestSign({
191
+ masterPubkey: iss, proxyUrl: qr.proxy, device, cert,
192
+ payload: { hola: 'mundo' }
193
+ })
194
+ ```
195
+
196
+ ### Secretos de servicios (los servicios del ecosistema son clientes identificados)
197
+
198
+ Los servicios (proxy, geo, bots…) **no llevan secretos de terceros en su `.env`**:
199
+ se enrolan al vault como un dispositivo más, con un cert limitado al scope
200
+ `vault:secrets:<ns>` (`pair --service <ns>`), y al arrancar piden su bundle.
201
+
202
+ Son **dos momentos distintos, a propósito**: el **enrolamiento** (registro de la
203
+ máquina) es un **comando previo** que corre un humano **una sola vez**; el
204
+ **arranque** de la app solo lee la identidad ya guardada y no interactúa con nadie.
205
+
206
+ #### 1) Enrolamiento — comando previo, una vez por máquina
207
+
208
+ ```bash
209
+ # en el VAULT (tu PC)
210
+ dotrino-vault pair --service proxy # invitación con scope SOLO vault:secrets:proxy
211
+ dotrino-vault secret set proxy TURN_KEY_ID …
212
+
213
+ # en la MÁQUINA del servicio (pega la invitación; te MUESTRA un código)
214
+ npx dotrino-env enroll --ns proxy
215
+
216
+ # de vuelta en el VAULT: tipeas el código LEYÉNDOLO de la pantalla del servicio
217
+ dotrino-vault approve 7K3F-92Q1
218
+ ```
219
+
220
+ Deja `~/.dotrino/service/<ns>/service-identity.json` (0600) con la llave del
221
+ dispositivo (generada ahí, nunca sale) + el cert. **No hay ningún secreto en disco.**
222
+
223
+ **Por qué NO se enrola en el primer arranque de la app:** el enrolamiento exige un
224
+ humano que **lea el código en la pantalla del servicio** — es lo único que impide que
225
+ un vault falso (que nunca vio el código) enrole la máquina, o que alguien apruebe a
226
+ ciegas. Un servicio arranca bajo systemd/PM2, sin TTY y sin nadie mirando: el código
227
+ acabaría en un log (y quien lea el log ya podría aprobar). Además el arranque quedaría
228
+ bloqueado esperando una aprobación que quizá nadie da, y un reinicio automático de
229
+ madrugada intentaría re-enrolar. Separados, el arranque es determinista e idempotente:
230
+ solo **lee**; el enrolamiento **escribe** y consume una invitación de un solo uso.
231
+
232
+ Va donde va el `npm ci` al aprovisionar el VPS. (Para máquinas efímeras —Docker,
233
+ autoescalado— haría falta una invitación pre-provisionada de un solo uso con TTL corto
234
+ y auto-aprobación; **aún no está decidido ni implementado**.)
235
+
236
+ #### 2) Arranque — sin interacción, en cada reinicio
237
+
238
+ ```js
239
+ import '@dotrino/vault/config' // como `dotenv/config`, pero contra el vault (ns = DOTRINO_NS)
240
+ console.log(process.env.TURN_KEY_ID)
241
+ ```
242
+
243
+ o explícito, con `@dotrino/vault/env` / `@dotrino/vault/service`:
244
+
245
+ ```js
246
+ import { loadEnv } from '@dotrino/vault/env'
247
+ const { secrets } = await loadEnv({ ns: 'proxy', required: ['TURN_KEY_ID'] })
248
+ ```
249
+
250
+ **Modos de fallo (importante):**
251
+
252
+ - **Vault caído / proxy caído** → **espera** (reintento con backoff, para siempre). Sin
253
+ vault el servicio no opera: no arranca con secretos viejos ni vacíos.
254
+ - **Sin enrolar, cert revocado o vencido, scope equivocado** → **aborta en el acto**.
255
+ Son errores que no se arreglan reintentando: hay que (re)enrolar.
256
+
257
+ CLI de apoyo: `dotrino-env status` (qué hay enrolado aquí), `dotrino-env check` (lista
258
+ los **nombres** de los secretos, nunca los valores), `dotrino-env run -- <cmd>` (inyecta
259
+ los secretos en el entorno de un proceso que no es Node).
260
+
261
+ Garantías: la petición va firmada por la llave del servicio + cert (scope solo
262
+ su `ns`); la respuesta viaja **sellada** a una llave efímera por petición (el
263
+ proxy que la transporta no puede leerla, y un replay no se puede descifrar) y
264
+ **firmada por la maestra** (verificada contra la `iss` pineada del enrolamiento).
265
+ Revocar el cert (`revoke`) corta el acceso de inmediato; `activity` audita cada
266
+ lectura. Los servicios críticos sin secretos en su core (el propio proxy) arrancan
267
+ sin vault; solo la feature que los necesita (TURN) espera. Primer consumidor:
268
+ `dotrino-proxy` (TURN de Cloudflare, ver su README).
269
+
270
+ ## Alcance
271
+
272
+ - **v1 (este):** servicio headless en Node (Linux), **multi-perfil**, con
273
+ **contraseña opcional por perfil para editarlo** (verificador PBKDF2) pero la
274
+ **clave privada en claro** en el disco (en `~/.local/share/dotrino/vault`, permisos
275
+ `0600`). Enrolamiento de dispositivos, firma delegada y lectura del árbol de
276
+ contenidos por el proxy. Distribución como binario único (Node SEA) + servicio systemd.
277
+ - **v2:** **cifrado en reposo** con la contraseña (keychain del SO o archivo, a
278
+ elección) — hoy la contraseña es un candado de edición, no cifra la clave; UI de
279
+ escritorio (Tauri) como cliente del daemon; firma de documentos con sellado de
280
+ tiempo (`dotrino-signer`); macOS y Windows.
281
+
282
+ ## Estructura
283
+
284
+ - `src/vault.js` — núcleo de UN perfil (Identity + transporte + router: enrolar/firmar/leer).
285
+ - `src/profiles.js` — registro multi-perfil (`profiles.json`, `p/<id>/`) + candado por contraseña.
286
+ - `src/manager.js` — corre todos los perfiles a la vez (uno por maestra/conexión).
287
+ - `src/daemon.js` — modo servicio: `state.json`, emparejamiento por señal, apagado limpio.
288
+ - `src/ctl.js` — CLI de control (habla con el daemon por archivos + señales, sin socket).
289
+ - `src/vaultControl.js` — API de control programática (misma vía que la CLI); la usa la TUI.
290
+ - `src/tui/` — interfaz de terminal a pantalla completa, sin dependencias (`term.js` primitivas ANSI/raw-mode; `app.js` pantallas).
291
+ - `src/transport.js` — conexión headless al proxy + `identify` firmado.
292
+ - `src/store.js` — árbol de contenidos (`vault.json`, versionado).
293
+ - `src/client.js` — helper de **dispositivo** (enrolar / pedir firma / leer).
294
+ - `src/protocol.js` — tipos de mensaje y scopes. · `src/qr.js` — QR ASCII. · `src/paths.js` — dirs.
295
+ - `bin/sea-entry.js` — entrypoint del binario único (multicall daemon / `--ctl` / `--tui`).
296
+ - `bin/dotrino-vaultd.js` — entrypoint de desarrollo (node directo).
297
+ - `bin/dotrino-vault-tui.js` — entrypoint de desarrollo de la TUI.
298
+ - `packaging/` — `build.sh` (binario), `install.sh`/`uninstall.sh`, unit systemd.
299
+ - `web/` — la página `vault.dotrino.com` (Vite + Vue).
300
+
301
+ Sin anuncios, sin cuentas, sin rastreo. MIT · parte de Dotrino.
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dotrino-vault-tui — interfaz de terminal (pantalla completa) del vault.
4
+ *
5
+ * Entrypoint de DESARROLLO (node directo). En producción el binario SEA expone
6
+ * lo mismo con `dotrino-vaultd --tui` (ver bin/sea-entry.js).
7
+ *
8
+ * NO abre la identidad ni la red: le da órdenes al daemon por archivos + señales
9
+ * (misma vía que la CLI de control). El daemon debe estar corriendo; si no, la
10
+ * TUI ofrece arrancarlo.
11
+ *
12
+ * node bin/dotrino-vault-tui.js
13
+ *
14
+ * Env:
15
+ * DOTRINO_VAULT_DIR dir de datos (default ~/.local/share/dotrino/vault)
16
+ */
17
+ import { runTui } from '../src/tui/app.js'
18
+
19
+ if (!process.stdout.isTTY) {
20
+ console.error('dotrino-vault-tui necesita un terminal interactivo (TTY).')
21
+ process.exit(2)
22
+ }
23
+
24
+ runTui().catch((e) => {
25
+ // El finally de runTui ya restauró el terminal; aquí solo reportamos.
26
+ console.error('error en la TUI:', e?.stack || e?.message || e)
27
+ process.exit(1)
28
+ })
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dotrino-vault — el CLI de control, instalado por npm.
4
+ *
5
+ * En el binario autosuficiente (SEA) este mismo CLI vive dentro del ejecutable y se
6
+ * invoca con `dotrino-vaultd --ctl …`; instalado por npm es un `bin` propio, que es lo
7
+ * que espera quien lo corre con `npx` o tras un `npm i -g`.
8
+ *
9
+ * NO abre la identidad ni la red: le da órdenes al daemon escribiendo archivos en el
10
+ * dir de datos (ver `src/vaultControl.js`). Por eso funciona igual en Linux, Windows,
11
+ * macOS y —montando el dir— desde fuera de un contenedor.
12
+ *
13
+ * dotrino-vault status · pair · approve <código> · members · devices · revoke <nonce>
14
+ *
15
+ * Env:
16
+ * DOTRINO_VAULT_DIR dir de datos (default: ver `src/paths.js`)
17
+ */
18
+ import { runCtl } from '../src/ctl.js'
19
+
20
+ runCtl(process.argv.slice(2)).catch((e) => {
21
+ console.error('error:', e?.message || e)
22
+ process.exit(1)
23
+ })
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dotrino-vaultd — entrypoint de DESARROLLO (node directo, sin SEA).
4
+ *
5
+ * node bin/dotrino-vaultd.js arranca el vault (modo servicio)
6
+ * node bin/dotrino-vaultd.js --pair arranca e imprime un QR de emparejamiento
7
+ *
8
+ * En producción el usuario corre el binario SEA (bin/sea-entry.js → src/daemon.js).
9
+ * Este archivo comparte el mismo núcleo (`runDaemon`) para no divergir.
10
+ *
11
+ * Env:
12
+ * DOTRINO_VAULT_DIR dir de datos (default ~/.dotrino/vault)
13
+ * PROXY_URL proxy (default wss://proxy.dotrino.com)
14
+ */
15
+ import { runDaemon } from '../src/daemon.js'
16
+ import { qrToString } from '../src/qr.js'
17
+
18
+ const mgr = await runDaemon()
19
+
20
+ // Atajo de dev: --pair imprime el QR directo en stdout (en producción se usa el CLI).
21
+ // Empareja contra el perfil ACTIVO.
22
+ if (process.argv.includes('--pair')) {
23
+ const { qr, expiresInMs } = mgr.current().startPairing({ label: 'cli' })
24
+ console.log(`\nEmparejá un dispositivo (válido ${expiresInMs / 60000} min):\n`)
25
+ console.log(qrToString(JSON.stringify(qr)))
26
+ console.log(JSON.stringify(qr))
27
+ }
28
+
29
+ console.log('\n(Ctrl+C para detener)')
@@ -0,0 +1,29 @@
1
+ /**
2
+ * sea-entry.js — punto de entrada ÚNICO del binario autosuficiente (SEA).
3
+ *
4
+ * El mismo binario hace de daemon y de CLI de control (multicall):
5
+ * dotrino-vaultd → arranca el daemon (modo servicio)
6
+ * dotrino-vaultd --ctl ... → CLI de control (status / pair / devices / revoke)
7
+ *
8
+ * El wrapper `dotrino-vault` invoca siempre con `--ctl`. Se usa import dinámico
9
+ * para no cargar el grafo del daemon (transporte/identity) cuando solo quieres
10
+ * un `status` rápido.
11
+ */
12
+ const argv = process.argv.slice(2)
13
+
14
+ if (argv[0] === '--tui') {
15
+ // Interfaz de terminal (pantalla completa). Como la CLI, NO toca la identidad
16
+ // ni el proxy: habla con el daemon por el dir de datos + señales.
17
+ if (!process.stdout.isTTY) { console.error('la TUI necesita un terminal interactivo (TTY).'); process.exit(2) }
18
+ import('../src/tui/app.js').then(({ runTui }) => runTui())
19
+ .catch((e) => { console.error('error en la TUI:', e.message); process.exit(1) })
20
+ } else if (argv[0] === '--ctl') {
21
+ // CLI de control: NO toca la identidad ni el proxy; habla con el daemon vía
22
+ // el dir de datos (state.json) y señales (SIGUSR1 para emparejar).
23
+ import('../src/ctl.js').then(({ runCtl }) => runCtl(argv.slice(1)))
24
+ .catch((e) => { console.error('error:', e.message); process.exit(1) })
25
+ } else {
26
+ // Modo daemon (servicio).
27
+ import('../src/daemon.js').then(({ runDaemon }) => runDaemon(argv))
28
+ .catch((e) => { console.error('error fatal del daemon:', e.message); process.exit(1) })
29
+ }
package/lib/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # @dotrino/vault
2
+
3
+ Usa **este dispositivo (navegador) como bóveda/CA** del ecosistema Dotrino, sin un PC
4
+ con el daemon. Es la contraparte browser del daemon `dotrino-vault`: atiende el mismo
5
+ protocolo de enrolamiento endurecido por el proxy y firma certificados de delegación
6
+ `D ← P` (donde `P` es la identidad de este dispositivo, `@dotrino/identity`).
7
+
8
+ Pensado para que **cualquier app** del ecosistema (no solo la terminal) pueda ofrecer
9
+ "usar este dispositivo como bóveda".
10
+
11
+ ## Uso
12
+
13
+ ```js
14
+ import { Identity } from '@dotrino/identity'
15
+ import { startDeviceVault } from '@dotrino/vault'
16
+
17
+ const identity = await Identity.connect()
18
+ const vault = await startDeviceVault(identity) // se conecta al proxy como P
19
+
20
+ // 1) Abrir un emparejamiento y mostrar el QR/JSON al dispositivo a enrolar:
21
+ const { qr } = vault.startPairing({ label: 'mi-agente' })
22
+ // El dispositivo (p. ej. @dotrino/identity#enrollDevice) consume `qr`, GENERA un
23
+ // código aleatorio y lo MUESTRA (no lo envía).
24
+
25
+ // 2) Cuando el dispositivo pide acceso, aparece en la lista de pendientes:
26
+ vault.onPendingChange(() => {
27
+ for (const { deviceId } of vault.listPending()) {
28
+ // Un humano LEE el código del dispositivo y lo TIPEA aquí:
29
+ // await vault.approve(deviceId, codigoTipeado)
30
+ }
31
+ })
32
+
33
+ // 3) Máquinas ya enroladas / revocar:
34
+ const machines = await vault.listMachines() // [{ sub, deviceId, label, exp, nonce, scope }]
35
+ // await vault.revoke(nonce)
36
+
37
+ vault.close()
38
+ ```
39
+
40
+ ## Credenciales del vault en vez del `.env` (Node)
41
+
42
+ La cara "dotenv" del paquete: **cualquier proyecto Node** jala sus credenciales del
43
+ vault del dueño y las deja en `process.env`. En el disco del servicio **no queda
44
+ ningún secreto**: solo la llave del dispositivo (generada ahí, nunca sale) y un
45
+ certificado con scope `vault:secrets:<ns>`. Los valores viven **solo en memoria**;
46
+ si la máquina se compromete, revocas el cert y no había nada que robar.
47
+
48
+ ### 1) Registro del cliente (una sola vez)
49
+
50
+ ```bash
51
+ # en el VAULT (tu PC): abres el emparejamiento del servicio y cargas sus secretos
52
+ dotrino-vault pair --service miapp # invitación con scope SOLO vault:secrets:miapp
53
+ dotrino-vault secret set miapp API_KEY sk-…
54
+
55
+ # en el PROYECTO/servidor: enrola esta máquina (pega la invitación)
56
+ npx dotrino-env enroll --ns miapp
57
+ # → muestra un código: dotrino-vault approve 7K3F-92Q1
58
+
59
+ # de vuelta en el VAULT: lo tipeas leyéndolo de esa pantalla
60
+ dotrino-vault approve 7K3F-92Q1
61
+ ```
62
+
63
+ El código lo **genera el servicio** y **no viaja** por la red: el vault solo puede
64
+ echarlo de vuelta si un humano lo tipeó. Así, un vault falso no puede enrolarte y
65
+ aprobar a ciegas no enrola a nadie. Queda `~/.dotrino/service/<ns>/service-identity.json`
66
+ (0600) con `{ device, cert, iss, proxy, ns }`.
67
+
68
+ Es un **comando previo**, no el primer arranque de la app: el enrolamiento necesita a
69
+ un humano leyendo el código en esta pantalla (bajo systemd/PM2 no hay TTY y el código
70
+ acabaría en un log), bloquea esperando la aprobación y **escribe** en disco consumiendo
71
+ una invitación de un solo uso. El arranque, en cambio, solo **lee** la identidad ya
72
+ guardada: es idempotente y no interactúa con nadie. Corre el `enroll` donde corres el
73
+ `npm ci` al aprovisionar la máquina.
74
+
75
+ ### 2) En el código
76
+
77
+ ```js
78
+ import '@dotrino/vault/config' // como `dotenv/config`, pero contra el vault (ns = DOTRINO_NS)
79
+ console.log(process.env.API_KEY)
80
+ ```
81
+
82
+ o explícito:
83
+
84
+ ```js
85
+ import { loadEnv } from '@dotrino/vault/env'
86
+ const { secrets } = await loadEnv({ ns: 'miapp', required: ['API_KEY'] })
87
+ ```
88
+
89
+ Es **asíncrono a propósito**: el `import` bloquea el arranque (top-level await) hasta
90
+ que los secretos estén. Si el vault no está disponible, **espera** (reintento con
91
+ backoff) — un servicio sin vault no arranca, no opera con secretos viejos ni vacíos.
92
+ Un fallo NO transitorio (sin enrolar, cert revocado, scope equivocado) sí aborta.
93
+
94
+ Para procesos que no son Node, el CLI los inyecta en el entorno de un hijo:
95
+
96
+ ```bash
97
+ dotrino-env run --ns miapp -- ./mi-binario
98
+ ```
99
+
100
+ ### API `@dotrino/vault/env`
101
+
102
+ - `loadEnv({ ns?, dir?, override?, wait?, required?, onRetry? }) → { ns, secrets, injected, skipped }`
103
+ (por defecto **no pisa** variables ya presentes en el entorno; `override: true` sí)
104
+ - `serviceDir(ns)`, `serviceRoot()`, `listEnrolled()`, `resolveNs(ns?)`
105
+ - Entorno: `DOTRINO_NS` · `DOTRINO_ENV_DIR` · `DOTRINO_ENV_HOME` · `DOTRINO_ENV_QUIET`
106
+ - CLI: `dotrino-env enroll|status|check|run` (`check` lista **nombres** de secretos, nunca valores)
107
+
108
+ Bajo el capó es `@dotrino/vault/service` (`enrollService` / `waitForSecrets`): petición
109
+ firmada por la llave del servicio + cert, respuesta **sellada** (ECDH efímero + AES-GCM,
110
+ el proxy no ve los valores) y **firmada por la maestra**, verificada contra la `iss`
111
+ pineada en el enrolamiento.
112
+
113
+ ## Modelo de aprobación (seguro por diseño)
114
+
115
+ - El **dispositivo** que se enrola genera un **código aleatorio** (`makePairingCode`) y
116
+ lo **muestra**; el código **no viaja** por la red.
117
+ - Esta bóveda **no conoce** el código: un humano lo **lee del dispositivo** y lo **tipea**
118
+ aquí. Al aprobar, la bóveda firma el cert y **echa** el código tipeado de vuelta.
119
+ - El dispositivo acepta el cert **solo si el código echado coincide** con el que generó.
120
+ Así, una bóveda falsa (que nunca vio el código) no puede enrolarlo, y **aprobar a ciegas**
121
+ (sin ir a leer el código del dispositivo) no enrola a nadie.
122
+
123
+ ## API
124
+
125
+ `startDeviceVault(identity, { proxyUrl? }) → Promise<handle>`
126
+
127
+ - `startPairing({ scope?, ttlMs?, label? }) → { qr, expiresInMs }`
128
+ - `listPending() → [{ deviceId, label }]`
129
+ - `approve(deviceId, code) → Promise<{ ok, deviceId }>` (code = lo que muestra el dispositivo)
130
+ - `reject(deviceId)`
131
+ - `listMachines() → Promise<[{ sub, deviceId, label, scope, exp, nonce }]>`
132
+ - `revoke(nonce) → Promise`
133
+ - `getSelfCert() → Promise<cert>` (self-cert `P ← P`, para actuar además de cliente)
134
+ - `onPendingChange(fn)`, `close()`
135
+
136
+ Cripto y firma: `@dotrino/identity`. Transporte: `@dotrino/proxy-client`. No reimplementa
137
+ nada del ecosistema.
138
+
139
+ MIT · parte de [Dotrino](https://dotrino.com).
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `import '@dotrino/vault/config'` — el equivalente de `import 'dotenv/config'`,
3
+ * pero contra el vault del dueño.
4
+ *
5
+ * Bloquea el arranque (top-level await) hasta que los secretos del ns estén en
6
+ * `process.env`. Si el vault no está disponible, ESPERA (reintento con backoff):
7
+ * la regla del ecosistema es que un servicio sin vault no arranca — no opera con
8
+ * secretos viejos ni vacíos. Un fallo NO transitorio (sin enrolar, cert revocado,
9
+ * scope equivocado) sí aborta el proceso.
10
+ *
11
+ * Config por entorno:
12
+ * DOTRINO_NS namespace de secretos (si no, el único enrolado en la máquina)
13
+ * DOTRINO_ENV_DIR directorio de la identidad del servicio (si no, ~/.dotrino/service/<ns>)
14
+ * DOTRINO_ENV_QUIET '1' para no imprimir la línea de arranque
15
+ */
16
+ import { loadEnv } from './env.js'
17
+
18
+ const quiet = process.env.DOTRINO_ENV_QUIET === '1'
19
+
20
+ const { ns, injected } = await loadEnv({
21
+ onRetry: (e, ms) => {
22
+ if (!quiet) console.error('[dotrino-env] vault no disponible (%s); reintentando en %ds…', e.message, Math.round(ms / 1000))
23
+ }
24
+ })
25
+
26
+ if (!quiet) console.error('[dotrino-env] %d secreto(s) del ns "%s" cargados en process.env', injected.length, ns)