@dotrino/vaultd 0.12.0 → 0.13.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 CHANGED
@@ -1,36 +1,139 @@
1
- # Dotrino Vault — tu certificador personal
1
+ # Dotrino Vault — tu bóveda personal
2
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.
3
+ > **Parte del ecosistema [Dotrino](https://dotrino.com).** Tu identidad y tu
4
+ > contenido, en tu máquina, bajo tus reglas — sin anuncios, sin cookies, sin rastreo.
5
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.
6
+ `dotrino-vault` es la **bóveda personal** del usuario: un **servicio headless** que
7
+ corre en tu propia máquina y hace dos cosas.
11
8
 
12
- ## Modelo: identidad delegada (la maestra se queda en una sola máquina)
9
+ - **Es tu certificador.** Custodia la llave que manda sobre tu cuenta y actúa como
10
+ tu **propia CA**: enrolas tus dispositivos, firmas por ti y avalas a otras
11
+ personas sin pedirle permiso a ningún portero central. En vez de depender de las
12
+ CAs, del "Inicia sesión con Google/Apple" o de un verificador de KYC, **certificas
13
+ tú**. Esa llave **nunca sale** de la máquina.
14
+ - **Es tu almacén.** Guarda el contenido de tus apps —hilos, "recientes", tu
15
+ perfil— **cifrado de punta a punta** con la clave de tu cuenta, y se lo sirve a
16
+ los dispositivos que tú conectaste. El proxy transporta el sobre; no puede
17
+ abrirlo.
18
+
19
+ **No escucha nada**: no abre puertos ni hay que tocar el router. Se conecta él hacia
20
+ afuera al proxy del ecosistema, y tus aparatos lo alcanzan por ahí.
21
+
22
+ **Este repo publica dos paquetes npm distintos**, con versiones independientes:
23
+
24
+ | Paquete | Dónde | Qué es |
25
+ |---|---|---|
26
+ | **`@dotrino/vaultd`** | la raíz | el daemon de este README: `dotrino-vaultd`, la CLI `dotrino-vault`, la TUI y el binario único. |
27
+ | **`@dotrino/vault`** | [`lib/`](./lib/) | la librería: usar **este dispositivo (navegador) como bóveda**, sin un PC con el daemon, y el **cliente de servicio** en Node (`/config`, `/env`, `/service`) con su CLI `dotrino-env`. Documentación propia en [`lib/README.md`](./lib/README.md). |
28
+
29
+ Que las versiones no coincidan es normal: son dos paquetes, no uno.
30
+
31
+ ## Modelo: un perfil es un acta, y una sola llave la sella
32
+
33
+ **Tres palabras para lo mismo, dicho una vez:** una **cuenta** es un **perfil** es un
34
+ **acta** — el conjunto de llaves miembro más la política firmada que dice qué puede
35
+ cada una. Un **miembro** es una **llave**, no un aparato: un mismo aparato puede
36
+ tener varias llaves y por lo tanto varias cuentas. La TUI llama **bóvedas** a esos
37
+ perfiles porque cada uno tiene su llave, su directorio y sus dispositivos; la
38
+ **bóveda** a secas es este servicio, que los atiende todos a la vez.
13
39
 
14
40
  ```
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
41
+ perfil (= cuenta) ── nombre estable: profileId = pubkey de la llave génesis
42
+ └── ACTA firmada: quién es miembro y qué puede cada uno
43
+ · sellador (master): UNA sola llave firma el acta. La intención es que sea la bóveda.
44
+ · miembros: una LLAVE cada uno, con capacidades
45
+ sign · store · read → un dispositivo tuyo
46
+ secrets + cn → un servicio: abre SOLO su propio cajón
47
+ · llavero: la clave de contenido del perfil, envuelta para cada miembro
48
+
49
+ PC (bóveda) ── su llave NUNCA sale ──────────────────────────────────────┐
50
+ · sella el acta (si es el master) y emite un CERT por miembro: │
51
+ "D puede <scope> en nombre de esta llave, hasta <exp>, revocable por <nonce>"
52
+ · firma datos a pedido de un miembro enrolado (devuelve solo la firma) │
53
+ · abre y cierra el contenido con la clave del perfil │
54
+ ▲ proxy (sendByPubkey + cola offline 24 h) │
55
+ │ │
56
+ cel / laptop ── su propia llave (ninguna otra la ve) ──────────────────┘
57
+ · se empareja con un QR → entra al acta y recibe su cert
58
+ · firma cada acción con su llave y adjunta el cert; la bóveda verifica la cadena
25
59
  ```
26
60
 
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**.
61
+ **El nombre de la cuenta es el `profileId`**: la pubkey de la llave donde nació, y no
62
+ cambia nunca es lo que conocen la reputación, los contactos y todo lo que esa
63
+ cuenta firmó. **No tiene por qué ser la llave de la bóveda**: si la cuenta nació en
64
+ tu teléfono y la bóveda la adoptó, la bóveda es la que manda pero el nombre sigue
65
+ siendo el de la llave génesis. Lo que la bóveda pone en cada cert es su propia llave
66
+ (quien lo emitió), no el nombre de la cuenta.
30
67
 
31
- ## Instalación (Linux)
68
+ **Qué autoriza qué, hoy.** Para firmar, leer y guardar, la autorización sale del
69
+ **cert** (se verifica la cadena hasta la llave de la bóveda). El **acta** se comprueba
70
+ *encima* del cert en los **secretos de servicios**, donde el `cn` del miembro tiene
71
+ que decir que es ese servicio: dos cierres independientes, así el límite no depende
72
+ solo de qué cert se emitió un día.
32
73
 
33
- **Ubuntu / Debian — `.deb`** (lo más simple): descarga el `.deb` (versionado) desde
74
+ ```sh
75
+ dotrino-vault members # el acta: qué llaves son tuyas y qué puede hacer cada una
76
+ dotrino-vault caps <ID> +firma # cambia permisos (+firma -guarda +lee …)
77
+ ```
78
+
79
+ **Los certificados caducan y se renuevan solos.** Un cert dura **30 días**. Mientras
80
+ siga vigente y no esté revocado, el dispositivo pide uno fresco por su cuenta —misma
81
+ llave, mismos permisos— sin QR ni aprobación. Un cert **vencido o revocado ya no se
82
+ renueva**: ahí toca volver a emparejar. Así una máquina que te robaron y revocaste
83
+ queda fuera en cuanto expira, sin depender de que ella se porte bien.
84
+
85
+ **Si se pierde la llave que sella, se pierde la cuenta.** No hay recuperación, ni
86
+ relevo, ni frase de respaldo: es la consecuencia asumida de que las llaves no se
87
+ copian. Por eso la bóveda **no te deja borrar un perfil que ella manda si quedan
88
+ otros dispositivos dentro**: primero le pasas el mando a uno que esté conectado. Si
89
+ lo borraras así, los demás se quedarían con su llave y sin nadie que pueda volver a
90
+ firmar el acta, y la cuenta moriría para todos sin avisar.
91
+
92
+ La cripto **no se reimplementa**: vive en `@dotrino/identity` (`signDelegation`,
93
+ `verifyChain`, `makeDeviceKey`, más `acta` y `content`) y el transporte es
94
+ `@dotrino/proxy-client`. Lo que sí vive aquí es el **lado-bóveda del emparejamiento**
95
+ (`lib/src/enroll.js`): un solo sitio donde se decide a quién se le emite un
96
+ certificado, compartido por el daemon del PC, por `@dotrino/vault` —que convierte un
97
+ navegador en bóveda— y por la copia vendorizada del iframe de identidad.
98
+
99
+ Diseño completo en [`docs/acta-de-perfil.md`](./docs/acta-de-perfil.md).
100
+
101
+ ### Qué atiende la bóveda
102
+
103
+ Las peticiones de un dispositivo **ya enrolado** (`sign`, `store`, `get`, `devices`,
104
+ `renew`, `secrets`) van firmadas por él y con su certificado; se verifica la cadena,
105
+ que el cert no esté revocado y que la hora venga dentro de una ventana de **±5
106
+ minutos** (sin eso, un relay podía reproducir mensajes firmados viejos durante toda
107
+ la vida del cert). Los del emparejamiento son la excepción, porque ahí todavía no hay
108
+ cert: `hello` solo se contesta a quien presente el nonce de una sesión de
109
+ emparejamiento viva, y `enroll` va firmado por la llave del dispositivo como prueba
110
+ de posesión.
111
+
112
+ | Mensaje | Qué hace |
113
+ |---|---|
114
+ | `hello` | entrega la llave de la bóveda a quien presenta el nonce de la invitación corta, firmado |
115
+ | `enroll` · `acta.sealed` | los dos caminos de vinculación (entrar a una cuenta / que la bóveda adopte la del aparato) |
116
+ | `sign` | la llave firma un payload y devuelve **solo la firma** |
117
+ | `store` | el contenido del usuario: hilos, "recientes" y perfil, cifrado de punta a punta |
118
+ | `get` | lee un nodo del árbol de contenidos (`vault.json`) |
119
+ | `devices` | la lista de dispositivos **más el acta** y la cadena de versiones que le falte |
120
+ | `renew` | cert fresco a 30 días, misma llave y mismo scope |
121
+ | `secrets` | el bundle de un servicio, sellado a una llave efímera y firmado |
122
+
123
+ Y emite `revoked` (autoborrado firmado) y `secrets.changed` (aviso de rotación).
124
+
125
+ Lo que pasa queda anotado en `activity.log` (JSONL, rotado a ~1 MB): qué dispositivo
126
+ firmó, renovó o enroló, y qué se rechazó y por qué, **sin el contenido de lo
127
+ firmado**. Se lee con `dotrino-vault activity`.
128
+
129
+ ## Instalación
130
+
131
+ Tres vías, y todas dejan la misma bóveda: **instalador de Linux** (queda como
132
+ servicio), **un comando** (`npx`, en cualquier sistema con Node) y **Docker**. En
133
+ Linux la primera es la cómoda; en Windows y macOS todavía no hay instalador de un
134
+ clic, así que van por las otras dos.
135
+
136
+ **Ubuntu / Debian — `.deb`:** descarga el `.deb` (versionado) desde
34
137
  [Releases](https://github.com/imdotrino/dotrino-vault/releases/latest) y haz doble
35
138
  clic, o en la terminal:
36
139
 
@@ -38,7 +141,14 @@ clic, o en la terminal:
38
141
  sudo apt install ./dotrino-vault_*.deb
39
142
  ```
40
143
 
41
- **Otro Linux tarball:** descarga el binario autosuficiente y ejecuta el instalador:
144
+ Deja los binarios en `/usr/bin` e instala la unidad `systemd --user` en
145
+ `/usr/lib/systemd/user`, habilitada para **todos** los usuarios de la máquina (cada
146
+ uno con su propia bóveda en su `$HOME`): te arranca sola en tu **próximo inicio de
147
+ sesión**. Para levantarla ya, `systemctl --user start dotrino-vault`; si estabas
148
+ **actualizando**, el servicio viejo sigue corriendo y hay que reiniciarlo con
149
+ `systemctl --user restart dotrino-vault`.
150
+
151
+ **Otro Linux x64 con systemd — tarball:**
42
152
 
43
153
  ```sh
44
154
  tar xzf dotrino-vault-*-linux-x64.tar.gz
@@ -46,87 +156,155 @@ cd dotrino-vault-*-linux-x64
46
156
  sh install.sh
47
157
  ```
48
158
 
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.
159
+ Hace lo equivalente en tu `$HOME` (`~/.local/bin` + `~/.config/systemd/user`) y va un
160
+ paso más allá: lo **arranca en el acto** y activa `linger`, así el vault corre desde
161
+ el arranque de la máquina aunque no inicies sesión.
162
+
163
+ El binario del tarball y el `.deb` son **x64/amd64** y el instalador **necesita
164
+ systemd** (si no lo encuentra, se detiene y te dice cómo arrancar el daemon a mano).
165
+ Para ARM —una Raspberry— o para un Linux sin systemd, usa Docker o `npx`.
166
+
167
+ Ambos traen **Node embebido**: no necesitas instalar Node ni dependencias de npm. Lo
168
+ único que esperan del sistema es `libatomic1`, que casi todas las distribuciones
169
+ traen puesta; el `.deb` la declara y la instala sola si falta. Con el tarball, en un
170
+ sistema muy pelado (un contenedor mínimo), instálala tú: `sudo apt install
171
+ libatomic1`.
172
+
173
+ **Cualquier sistema con Node — un comando:**
174
+
175
+ ```sh
176
+ npx -y @dotrino/vaultd # Node ≥ 20
177
+ npx -y @dotrino/vaultd --tui # la bóveda y su pantalla de control en la misma ventana
178
+ ```
179
+
180
+ Es la vía normal en **Windows y macOS**, y sirve igual en Linux. Arranca en primer
181
+ plano: la bóveda vive mientras dejes esa ventana abierta. Los comandos de control se
182
+ anteponen con `npx -p @dotrino/vaultd` (por ejemplo, `npx -p @dotrino/vaultd
183
+ dotrino-vault pair`). Si no tienes Node, el instalador del ecosistema lo baja en tu
184
+ carpeta, sin permisos de administrador, y corre el paquete:
185
+
186
+ ```sh
187
+ # Linux y macOS
188
+ curl -fsSL https://install.dotrino.com/install.sh | sh -s -- @dotrino/vaultd
189
+ # Windows (PowerShell)
190
+ & ([scriptblock]::Create((irm https://install.dotrino.com/install.ps1))) @dotrino/vaultd
191
+ ```
192
+
193
+ **Docker (cualquier sistema, y la única vía *empaquetada* para ARM — el `.deb` y el
194
+ tarball son solo x64; `npx` también sirve en una Raspberry):**
195
+
196
+ ```sh
197
+ docker volume create dotrino-vault
198
+ docker run -d --name dotrino-vault --restart unless-stopped \
199
+ -v dotrino-vault:/data ghcr.io/imdotrino/dotrino-vault
200
+
201
+ docker exec -it dotrino-vault dotrino-vault pair # conectar un aparato
202
+ ```
52
203
 
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).
204
+ La imagen se publica en GHCR en cada versión, para amd64 y arm64 (una Raspberry
205
+ encendida en casa es el caso normal, no el exótico). No abre ningún puerto y el CLI
206
+ entra por `docker exec` porque le habla al daemon por archivos del dir de datos, no
207
+ por un socket. **El volumen ES tu cuenta:** si lo borras, esa identidad no se
208
+ recupera.
61
209
 
62
210
  > Sin firma de código: tu sistema puede advertir que el binario no está firmado. Es
63
211
  > 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.
212
+ > instalador lo da). Hay un build de Windows (`packaging/build-win.sh`) pero **no se
213
+ > publica**: se construye desde Linux y no se ha probado en un Windows de verdad, y
214
+ > la inyección del blob invalida la firma de Microsoft, así que saltaría SmartScreen.
215
+
216
+ **Dónde vive todo y cómo se para.** Tus datos —llave incluida— viven en
217
+ `~/.local/share/dotrino/vault` (Linux y macOS), `%LOCALAPPDATA%\Dotrino\vault`
218
+ (Windows) o `/data` dentro del volumen (Docker), siempre con permisos `0600`/`0700` y
219
+ un subdirectorio `p/<id>/` por perfil. Se mueve con `DOTRINO_VAULT_DIR`. Arrancar y
220
+ parar depende de cómo lo instalaste: `systemctl --user {start,stop,restart}
221
+ dotrino-vault`, `docker restart dotrino-vault`, o cerrar la ventana del `npx` y
222
+ volver a correrlo. El propio CLI te dice cuál te toca cuando el daemon no está.
65
223
 
66
224
  ### CLI de control
67
225
 
68
226
  ```sh
69
227
  dotrino-vault tui # interfaz de terminal a pantalla completa (ver abajo)
70
228
  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)
229
+ dotrino-vault pair # empareja: muestra el QR (más la URL y un código pegable) y espera
230
+ dotrino-vault pair --new-account [nombre] # estrena una cuenta VACÍA aquí y mete al dispositivo en ELLA
231
+ dotrino-vault pair --adopt [nombre] # al revés: la bóveda se queda con la cuenta que trae el aparato
232
+ dotrino-vault pair --save [archivo] # además, escribe la invitación en un .dpair para llevarla
233
+ dotrino-vault pending # qué dispositivo está esperando (su identificador) y cómo aprobarlo
234
+ dotrino-vault approve <código> # aprueba tecleando los 6 dígitos que MUESTRA el dispositivo
74
235
  dotrino-vault reject <deviceId> # rechaza un dispositivo pendiente
75
236
  dotrino-vault devices # lista dispositivos enrolados / revocados
237
+ dotrino-vault members # el acta del perfil: qué llaves son tuyas y qué puede cada una
238
+ dotrino-vault caps <ID> ±permiso # cambia lo que puede un dispositivo (+firma -guarda +lee …)
76
239
  dotrino-vault revoke <nonce> # revoca un dispositivo (le ordena autoborrarse)
240
+ dotrino-vault activity [n] # bitácora de seguridad: firmas, renovaciones, enrolados, rechazos
77
241
  dotrino-vault pair --service <ns> # empareja un SERVICIO (proxy, geo…) con acceso SOLO a sus secretos
78
242
  dotrino-vault secret set <ns> <CLAVE> <valor> # guarda un secreto para ese servicio
79
243
  dotrino-vault secret rm <ns> <CLAVE> # borra un secreto
80
244
  dotrino-vault secret list # nombres de secretos (nunca valores)
81
- dotrino-vault logs # últimos logs del servicio
245
+ dotrino-vault logs # últimas 40 líneas del servicio (journalctl; solo donde hay systemd)
246
+ dotrino-vault version # versión instalada (status avisa si el daemon quedó viejo)
82
247
  ```
83
248
 
249
+ `approve` recibe el **código**, no el `deviceId`: la bóveda no conoce el código —solo
250
+ su compromiso— y lo aprende cuando lo tecleas. El que sí recibe `deviceId` es
251
+ `reject`.
252
+
253
+ El `ns` de un secreto va en minúsculas (`[a-z0-9-]`, hasta 32), la clave en
254
+ MAYÚSCULAS_CON_GUION_BAJO (hasta 64) y el valor es texto de hasta 8 KB.
255
+
84
256
  ### Interfaz de terminal (TUI)
85
257
 
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:
258
+ Las bóvedas, los dispositivos y los secretos también se manejan desde una **interfaz
259
+ de terminal a pantalla completa**, sin memorizar subcomandos. El acta (`members`,
260
+ `caps`) y la bitácora (`activity`) todavía no están ahí: para eso, la CLI.
88
261
 
89
262
  ```sh
90
263
  dotrino-vault tui # binario instalado
264
+ dotrino-vaultd --tui # la bóveda y su interfaz en la MISMA ventana
265
+ # (si ya hay una corriendo, se engancha a ella)
91
266
  node bin/dotrino-vault-tui.js # en desarrollo (o: npm run tui)
92
267
  ```
93
268
 
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).
269
+ Como la CLI, la TUI **no abre la identidad ni la red**: le deja la orden al daemon en
270
+ un archivo del dir de datos (la señal es solo para que la atienda al instante; el
271
+ daemon vigila la carpeta igual, porque en Windows no hay señales). El daemon debe
272
+ estar corriendo; si no, la TUI ofrece arrancarlo con `S` — solo sabe hacerlo por
273
+ systemd, así que en Docker o fuera de Linux arráncalo tú.
98
274
 
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:
275
+ **Navegación en dos niveles**, para que siempre sea explícito de qué bóveda son los
276
+ dispositivos/variables que estás viendo:
101
277
 
102
278
  1. **Bóvedas** es la pantalla de entrada: lista tus perfiles (`↑↓` mover, `Enter`
103
279
  **entrar** a uno — lo activa si no lo estaba). Ahí también **conectas un
104
- dispositivo** con `p` (sin entrar: activa la bóveda elegida y abre la pregunta
105
- de a qué cuenta entra), creas una bóveda nueva, renombras, borras y
106
- pones/quitas/usas la contraseña (candado).
280
+ dispositivo** con `p` (sin entrar: activa la bóveda elegida y abre la pregunta de
281
+ a qué cuenta entra), creas una bóveda nueva, renombras, borras y pones/quitas/usas
282
+ la contraseña (candado).
107
283
  2. Al entrar caes en sus **pestañas horizontales**, que cambias con `←→`:
108
284
  - **Dispositivos (pares):** verlos, **emparejar** uno nuevo, **aprobar** con el
109
- código que muestra el dispositivo, **rechazar** y **revocar**. Al emparejar, la
110
- bóveda **pregunta primero a qué cuenta entra el dispositivo** —a esta, o a una
111
- cuenta nueva que se estrena para él— y recién después muestra el QR, que además
112
- dice de qué cuenta salió. (En la CLI: `dotrino-vault pair --new-account
113
- [nombre]`.)
285
+ código que muestra el dispositivo, **rechazar** y **revocar**.
114
286
  - **Scopes y variables (secretos):** ver los scopes y sus variables (nunca los
115
- valores), **agregar** una variable (con su scope) y **quitar** una variable o
116
- un scope entero.
287
+ valores), **agregar** una variable (con su scope) y **quitar** una variable o un
288
+ scope entero.
289
+
290
+ Al emparejar, la bóveda **pregunta primero a qué cuenta entra el dispositivo** y
291
+ recién después muestra el QR, que además dice de qué cuenta salió. La pregunta ofrece
292
+ una tercera opción, «adoptar la cuenta que trae el dispositivo», que aparece
293
+ **desactivada**: el dispositivo todavía no sabe entregar la suya. Desde la CLI ese
294
+ camino ya se inicia con `dotrino-vault pair --adopt`.
117
295
 
118
- `Esc` desde las pestañas vuelve a la lista de bóvedas (para entrar a otra); `q`
119
- sale desde cualquier pantalla. Las teclas de cada acción se listan en la barra
120
- inferior.
296
+ `Esc` desde las pestañas vuelve a la lista de bóvedas; `q` sale desde cualquier
297
+ pantalla, salvo mientras escribes en un campo o respondes una confirmación: ahí se
298
+ sale con `Esc` o `Ctrl+C`.
121
299
 
122
- **Idioma (`l`).** La TUI está en **español e inglés** y la tecla `l` conmuta entre
123
- los dos en cualquier pantalla (incluida la de "el daemon no está corriendo"). El
124
- idioma elegido se recuerda en `prefs.json` del dir de datos. Sin elección previa
125
- se toma del entorno (`DOTRINO_LANG`, o el locale `LC_ALL`/`LANG`), con el español
126
- por defecto.
300
+ **Idioma (`l`).** La TUI está en **español e inglés** y la tecla `l` conmuta entre los
301
+ dos en cualquier pantalla (incluida la de "el daemon no está corriendo"). El idioma
302
+ se recuerda en `prefs.json` del dir de datos, y al arrancar se decide en este orden:
303
+ `DOTRINO_LANG` (si la pones, manda en esa ejecución) `prefs.json` el locale del
304
+ sistema (`LC_ALL`, `LC_MESSAGES`, `LANGUAGE` o `LANG`) → español.
127
305
 
128
- **Las teclas NO cambian con el idioma**: son mnemónicos en **inglés** y valen igual
129
- en español (solo se traduce la palabra que las explica en la barra de ayuda).
306
+ **Las teclas NO cambian con el idioma**: son mnemónicos en **inglés** y valen igual en
307
+ español (solo se traduce la palabra que las explica en la barra de ayuda).
130
308
 
131
309
  | Tecla | Acción | Dónde |
132
310
  |---|---|---|
@@ -136,44 +314,52 @@ en español (solo se traduce la palabra que las explica en la barra de ayuda).
136
314
  | `d` | delete — borrar la bóveda | Bóvedas |
137
315
  | `p` | **pair — conectar un dispositivo** (desde Bóvedas entra directo, sin `Enter`) | Bóvedas · Dispositivos |
138
316
  | `c` | change password — poner/cambiar la contraseña | Bóvedas |
139
- | `x` | quitar: contraseña · dispositivo pendiente · variable/scope | todas |
317
+ | `x` | quitar: contraseña · dispositivo pendiente · variable/scope | Bóvedas · Dispositivos · Scopes · Emparejar |
140
318
  | `u` / `k` | unlock / locK — candado de la bóveda | Bóvedas |
141
319
  | `a` | approve — aprobar el dispositivo | Dispositivos · Emparejar |
142
320
  | `v` | reVoke — revocar un dispositivo enrolado | Dispositivos |
143
321
  | `y` | yes — confirmar (también se acepta `s`) | confirmaciones |
144
- | `b` / `Esc` | backvolver | pestañas · Emparejar |
322
+ | `n` / `Esc` / `Enter` | nocancelar la confirmación | confirmaciones |
323
+ | `Supr` | lo mismo que `d` (Bóvedas), `v` (Dispositivos) y `x` (Scopes) | listas |
324
+ | `RePág` `AvPág` `Inicio` `Fin` | mover de 5 en 5 · ir al principio o al final (en Emparejar, scroll del QR) | listas |
325
+ | `Ctrl+U` / `Ctrl+W` | limpiar el campo / borrar la última palabra | al escribir |
326
+ | `b` / `Esc` | back — volver | pestañas · Emparejar · ¿a qué cuenta entra? |
327
+ | `S` / `R` | arrancar el servicio / volver a comprobarlo | daemon detenido |
145
328
  | `l` | language — español ⇄ English | todas |
146
329
  | `q` | quit — salir | todas |
147
330
 
148
331
  ### Varios perfiles en el mismo PC
149
332
 
150
333
  Puedes tener varias identidades tuyas en la misma máquina (p. ej. personal y
151
- trabajo). Cada perfil es **una identidad distinta**: su propia clave, sus propios
334
+ trabajo). Cada perfil es **una cuenta distinta**: su propia llave, sus propios
152
335
  dispositivos, sus propios datos y secretos — nada se cruza entre ellos. **Todos
153
- atienden a la vez**: el perfil «activo» solo decide a cuál va un comando cuando no
154
- lo dices con `--profile`, no apaga a los demás.
336
+ atienden a la vez**: el perfil «activo» solo decide a cuál va un comando cuando no lo
337
+ dices con `--profile`, no apaga a los demás.
155
338
 
156
339
  ```sh
157
340
  dotrino-vault profile ls # lista los perfiles (* = el activo)
158
341
  dotrino-vault profile add Trabajo # crea un perfil (identidad nueva, vacía)
159
342
  dotrino-vault profile use Trabajo # elige el activo
160
343
  dotrino-vault profile rename <nombre> # renombra
161
- dotrino-vault profile rm Trabajo # BORRA el perfil y su identidad (irreversible)
344
+ dotrino-vault profile rm Trabajo # BORRA el perfil y su identidad (te pide escribir su nombre)
162
345
 
163
- dotrino-vault pair --profile Trabajo # cualquier comando acepta --profile
164
- dotrino-vault devices --profile personal
346
+ dotrino-vault pair --profile Trabajo # cualquier comando acepta --profile (o -p), en cualquier posición
347
+ dotrino-vault devices --profile personal # vale el id o el nombre (sin distinguir mayúsculas)
165
348
  ```
166
349
 
167
- Si ya usabas el vault antes de esto, tu identidad de siempre se convierte sola en
168
- el primer perfil («Perfil 1»): la misma clave, los mismos dispositivos, nada que
169
- volver a emparejar.
350
+ No se borra el único perfil, ni una cuenta que manda esta bóveda mientras le queden
351
+ otros dispositivos: primero le pasas el mando a uno conectado.
352
+
353
+ Si ya usabas el vault antes de esto, tu identidad de siempre se convierte sola en el
354
+ primer perfil («Perfil 1»): la misma llave, los mismos dispositivos, nada que volver
355
+ a emparejar.
170
356
 
171
357
  ### Contraseña del perfil (opcional)
172
358
 
173
359
  Cada perfil puede llevar contraseña. **Solo se pide para EDITAR el perfil** (cambiar
174
360
  tu nombre, avatar o datos): tus dispositivos siguen firmando, leyendo y guardando
175
- aunque el perfil esté bloqueado — así un reinicio del PC nunca deja tus apps
176
- muertas esperando a que alguien teclee algo.
361
+ aunque el perfil esté bloqueado — así un reinicio del PC nunca deja tus apps muertas
362
+ esperando a que alguien teclee algo.
177
363
 
178
364
  ```sh
179
365
  dotrino-vault profile password # pone o cambia la contraseña (te la pregunta)
@@ -184,66 +370,188 @@ dotrino-vault lock # vuelve a bloquear
184
370
 
185
371
  El perfil se vuelve a bloquear al reiniciar el servicio. La contraseña **no se
186
372
  guarda**: solo un verificador con sal (PBKDF2), igual que el candado del navegador.
187
- Para que quede claro qué protege y qué no: evita que otro que se siente en tu
188
- máquina —o un dispositivo tuyo comprometido— te reescriba el perfil; **no** cifra la
189
- clave en el disco (eso es el cifrado en reposo, ver *Alcance*).
190
-
191
- **Emparejamiento endurecido** (ver [`docs/pairing-protocol.md`](./docs/pairing-protocol.md)):
192
- el dispositivo prueba posesión de su llave firmando el enrolamiento, y la maestra
193
- solo firma el certificado **después** de que compares un código de 6 dígitos (SAS)
194
- entre las dos pantallas y corras `approve`. Un código robado ya no alcanza para
195
- entrar; la revocación de un dispositivo le ordena **autoborrarse** (con firma de la
196
- maestra, no por un mensaje cualquiera).
197
-
198
- La invitación viaja **comprimida** (`lib/src/invite.js`): los datos van en binario y el
199
- binario en base64url, ~100 caracteres en vez de los ~340 del JSON. Eso baja el QR de 69
200
- módulos a 41 —de 77×39 a 49×25 en la terminal— y de paso deja un código pegable de una
201
- sola palabra. El grueso del ahorro es la llave maestra: en el QR va el **punto comprimido**
202
- de la curva (33 bytes) y el lector rearma la JWK con una plantilla, comprobando que sale
203
- **byte a byte** igual, porque el proxy direcciona por esa string exacta. Si una llave no
204
- encaja en ninguna plantilla, la invitación sale en su forma larga: se hace grande, nunca
205
- incorrecta.
206
-
207
- El servicio se gestiona con systemd `--user`
208
- (`systemctl --user {start,stop,restart} dotrino-vault`). Tus datos —clave maestra
209
- incluida— viven en `~/.local/share/dotrino/vault` (permisos `0600`/`0700`), con un
210
- subdirectorio `p/<id>/` por perfil.
373
+ Tiene un mínimo de 4 caracteres y, tras 5 intentos fallidos, cada intento nuevo
374
+ espera cada vez más (hasta 5 minutos); la cuenta de fallos se guarda, así que
375
+ reiniciar no la borra.
376
+
377
+ Con el perfil bloqueado, la CLI **no** te pide la contraseña sobre la marcha:
378
+ `profile rename`, `profile rm` y `profile password` fallan con «perfil bloqueado» y
379
+ hay que correr `dotrino-vault unlock` antes. (La TUI la pide sola cuando hace
380
+ falta.)
381
+
382
+ Para que quede claro qué protege y qué no: evita que otro que se siente en tu máquina
383
+ —o un dispositivo tuyo comprometido— te reescriba el perfil; **no** cifra la llave en
384
+ el disco (de eso se encarga el cifrado en reposo, más abajo, que hoy tampoco usa la
385
+ contraseña).
386
+
387
+ ## Emparejar un aparato
388
+
389
+ **Vincular tiene exactamente dos caminos, y pregunta la bóveda antes de enseñar el
390
+ QR:**
391
+
392
+ - **El aparato entra a una cuenta de la bóveda** — la que elijas, o una nueva que se
393
+ estrena para él (`pair --new-account`). El aparato estrena una llave; nada de lo que
394
+ ya tenía se sobrescribe.
395
+ - **La bóveda adopta la cuenta del aparato** (`pair --adopt`): la cuenta sigue siendo
396
+ la misma para todo el mundo —el mismo `profileId`, la misma reputación— y lo que
397
+ cambia es quién sella. El aparato se queda como un miembro más.
398
+
399
+ **No existe fusionar dos cuentas**, ni al vincular ni después. El modo viaja dentro de
400
+ la invitación, el dispositivo lo repite firmado y la bóveda rechaza el que no coincida
401
+ con el que abrió. Detalle en
402
+ [`docs/vinculacion-de-cuentas.md`](./docs/vinculacion-de-cuentas.md).
403
+
404
+ **Emparejamiento endurecido.** El dispositivo prueba posesión de su llave firmando el
405
+ enrolamiento, y la bóveda solo firma el certificado **después** de que teclees el
406
+ **código de 6 dígitos que muestra el dispositivo**. El código no viaja: el dispositivo
407
+ lo sortea, lo enseña en su pantalla y manda solo su **compromiso**
408
+ `SHA-256(código‖llave‖sesión)`. La bóveda no lo conoce —no lo puede mostrar ni
409
+ comparar por su cuenta—; lo aprende cuando lo tecleas, recompone el compromiso y solo
410
+ si coincide firma el certificado. Aprobar exige, entonces, haber ido a leer la
411
+ pantalla del dispositivo. Al entregar el certificado la bóveda devuelve el código, y
412
+ el dispositivo lo rechaza si no es el suyo: una bóveda falsa, que nunca lo vio, no
413
+ puede enrolarlo. Un código robado ya no alcanza para entrar, y la revocación de un
414
+ dispositivo le ordena **autoborrarse** (con firma de la llave que manda, no por un
415
+ mensaje cualquiera).
416
+
417
+ > El porqué y las amenazas que cierra están en
418
+ > [`docs/pairing-protocol.md`](./docs/pairing-protocol.md). Ojo: ese documento es la
419
+ > decisión de diseño original y en dos puntos quedó atrás del código — el código de
420
+ > aprobación lo genera el dispositivo, no lo deriva la bóveda, y se aprueba
421
+ > tecleándolo, no comparándolo en dos pantallas.
422
+
423
+ **La cita del proxy.** Lo que va en el QR no es la dirección de la bóveda: es una
424
+ **cita** que emite el proxy — 6 caracteres, un solo uso, 5 minutos de vida; los 2
425
+ primeros dicen qué proxy la emitió, para poder canjearla desde otro nodo. El
426
+ dispositivo la canjea, obtiene la dirección real de la conexión y recién entonces le
427
+ pregunta a la bóveda quién es, presentando el nonce de la sesión; la respuesta va
428
+ firmada y con ese nonce dentro de lo firmado, así que no sirve la de otro
429
+ emparejamiento. Se gana doble: el QR no deja impresa ninguna dirección permanente, y
430
+ la dirección de verdad —34 caracteres, para poder rutearse entre proxies— no tiene que
431
+ caber en un código que se dicta.
432
+
433
+ La invitación viaja **comprimida** (`lib/src/invite.js`) y tiene dos formas. La que se
434
+ emite hoy es la **corta**: 21 caracteres, un enlace de 51, porque no lleva la llave ni
435
+ el nombre de la cuenta. Eso deja el QR en 29 módulos, que en la terminal son **39×20**.
436
+ Si el proxy no sabe emitir citas, la bóveda cae sola a la forma **compacta**: 91
437
+ caracteres, enlace de 121 y un QR de 41 módulos (51×26). Ahí sí viaja la llave, y el
438
+ grueso del ahorro es ella: va el **punto comprimido** de la curva (33 bytes) y el
439
+ lector rearma la JWK con una plantilla, comprobando que sale **byte a byte** igual,
440
+ porque el proxy direcciona por esa string exacta. Si una llave no encaja en ninguna
441
+ plantilla, la invitación sale en su forma larga (base64 del JSON): se hace grande,
442
+ nunca incorrecta. Las tres formas son una sola palabra en base64url, así que el mismo
443
+ texto sirve para el QR y para pegar.
444
+
445
+ ## Lo que guardas: el store del usuario
446
+
447
+ Un dispositivo enrolado con el permiso de guardar (`vault:store`) usa la bóveda como
448
+ su almacén: **hilos** de contenido (agregar, listar, borrar, exportar/importar), el
449
+ contador de **aperturas** que alimenta los "recientes" del hub, y tu **perfil** (apodo,
450
+ avatar, campos), del que la bóveda es la copia autoritativa — cada dispositivo lo
451
+ empuja al editarlo y lo jala al arrancar, así que ves el mismo perfil en todos.
452
+ Espeja el modelo de datos de `@dotrino/store`, guardado en `threads.json` dentro del
453
+ dir del perfil. Las lecturas se conforman con `vault:read`; escribir exige
454
+ `vault:store`.
455
+
456
+ El contenido que guardan las apps viaja **cifrado de punta a punta** con la clave de
457
+ contenido de tu perfil: el dispositivo cifra antes de enviar y la bóveda responde
458
+ cifrada con la misma clave. El proxy transporta el sobre pero no puede leerlo. Si
459
+ esta bóveda no tiene la clave de contenido del perfil, rechaza la operación en vez de
460
+ guardar en claro.
461
+
462
+ La excepción, dicha sin adornos: la sincronización del **perfil**
463
+ (`profileSet`/`profileGet`) todavía viaja **sin cifrar**, y una operación que llegue
464
+ en claro se guarda en claro. Es deuda, no diseño.
465
+
466
+ El candado de contraseña solo bloquea **editar el perfil**: guardar y leer contenido
467
+ siguen funcionando con el perfil bloqueado.
468
+
469
+ (El otro store, el «árbol de contenidos» de `vault.json`, es hoy un esqueleto: se
470
+ puede leer con `vault.get`, pero ningún mensaje del protocolo escribe nodos.)
471
+
472
+ ## Cifrado en reposo
473
+
474
+ **Todo** lo que la bóveda guarda va **cifrado** con AES-256-GCM y una clave derivada de
475
+ material de **esta máquina** (`/etc/machine-id` en Linux, `MachineGuid` en Windows,
476
+ `IOPlatformUUID` en macOS) más un salt local: `identity.json` (la maestra),
477
+ `vault.json` (el árbol de contenido), `threads.json` (hilos, aperturas y tu perfil) y
478
+ `secrets.json` (los secretos de servicios). Copiar cualquiera de esos archivos a otro
479
+ equipo **no sirve de nada**.
480
+
481
+ En la máquina del **servicio** pasa lo mismo con `service-identity.json`
482
+ (`@dotrino/vault/env`), que lleva la llave privada de su dispositivo.
483
+
484
+ La migración es sola y sin pedir nada: un archivo de una instalación anterior se lee
485
+ igual estando en claro y queda cifrado en la primera escritura (la identidad se migra
486
+ al arrancar, verificando que puede volver a leerse antes de reemplazar el original).
487
+ Queda fuera a propósito `activity.log`, la bitácora de auditoría: no guarda payloads
488
+ (solo op, dispositivo y hora) y se quiere legible para diagnosticar.
489
+
490
+ Lo que **no** resuelve, dicho sin adornos: no protege contra alguien con acceso a esta
491
+ misma máquina como tu usuario o como root — puede leer el mismo material que leemos
492
+ nosotros. Es subir el listón (de «copiar un archivo» a «tener tu máquina»), no una
493
+ imposibilidad criptográfica. Y hoy la **contraseña del perfil no participa** en esa
494
+ clave, aunque `machineKey` ya la acepta. Todos los archivos conservan además sus
495
+ permisos `0600` dentro de un dir `0700`.
211
496
 
212
497
  ## Desarrollo
213
498
 
214
499
  ```sh
215
- npm install
216
- node bin/dotrino-vaultd.js # arranca el daemon (modo servicio)
217
- node bin/dotrino-vaultd.js --pair # arranca + imprime un QR de emparejamiento
218
- bash packaging/build.sh # compila el binario único (dist/)
500
+ npm install # Node ≥ 20
501
+ node bin/dotrino-vaultd.js # arranca el daemon (modo servicio)
502
+ node bin/dotrino-vaultd.js --pair # arranca + imprime un QR de emparejamiento
503
+ node bin/dotrino-vaultd.js --tui # la bóveda y su pantalla de control, misma ventana
504
+ npm test # 118 pruebas (node --test, sin dependencias)
505
+
506
+ bash packaging/build.sh # binario único SEA + tarball de Linux (dist/)
507
+ bash packaging/build-deb.sh # el .deb (requiere dpkg-deb; construye el binario si falta)
508
+ bash packaging/build-win.sh # .exe + .zip de Windows, cruzado desde Linux (sin probar)
509
+ docker build -t dotrino-vault . # la imagen (la misma que CI publica en GHCR)
219
510
  ```
220
511
 
512
+ Los tests cubren lo que duele si se rompe: el emparejamiento y la comprobación del
513
+ código antes de firmar, la compresión de la invitación, el cifrado en reposo, el
514
+ aislamiento entre perfiles, el freno de borrado del acta, los secretos de punta a
515
+ punta y la precedencia del vault sobre el `.env`; también el render y el bilingüe de
516
+ la TUI. **Tres son de extremo a extremo contra el proxy de verdad** y esperan el repo
517
+ hermano en `../dotrino-proxy` (`secrets.e2e`, `multiprofile.e2e`,
518
+ `pairing-cita.e2e`): no se saltan solos, así que sin ese repo al lado `npm test`
519
+ falla.
520
+
521
+ Etiquetar `vaultd-v<ver>` publica la imagen de Docker en GHCR; el `.deb` y el tarball
522
+ se suben a la release a mano.
523
+
221
524
  ### Enrolar y usar desde un dispositivo (Node, para testing)
222
525
 
223
526
  ```js
224
- import { enroll, requestSign } from 'dotrino-vault/src/client.js'
527
+ import { enroll, requestSign } from './src/client.js'
225
528
 
226
- // 1) escaneas el QR del vault → obtienes { iss, proxy, token }
227
- const { device, cert, iss } = await enroll({ qr }) // GUARDA device (privada) + cert
529
+ // 1) lees la invitación LARGA del vault → { iss, proxy, token, sn }
530
+ // (este helper es solo para pruebas: no sabe canjear la cita de la invitación
531
+ // corta. Ese camino lo implementan `lib/src/service.js` y `@dotrino/identity`.)
532
+ const { device, cert, iss } = await enroll({
533
+ qr,
534
+ onChallenge: ({ deviceId, code }) => console.log(deviceId, '→ teclea en el vault:', code)
535
+ }) // GUARDA device (privada) + cert
228
536
 
229
- // 2) le pides a la maestra que firme algo (la maestra nunca sale del vault)
537
+ // 2) le pides a la bóveda que firme algo (su llave nunca sale)
230
538
  const { signature } = await requestSign({
231
539
  masterPubkey: iss, proxyUrl: qr.proxy, device, cert,
232
540
  payload: { hola: 'mundo' }
233
541
  })
234
542
  ```
235
543
 
236
- ### Secretos de servicios (los servicios del ecosistema son clientes identificados)
544
+ ## Secretos de servicios
237
545
 
238
- Los servicios (proxy, geo, bots…) **no llevan secretos de terceros en su `.env`**:
239
- se enrolan al vault como un dispositivo más, con un cert limitado al scope
240
- `vault:secrets:<ns>` (`pair --service <ns>`), y al arrancar piden su bundle.
546
+ Los servicios del ecosistema (proxy, geo, bots…) **no llevan secretos de terceros en
547
+ su `.env`**: se enrolan a la bóveda como un miembro más, con un cert limitado al scope
548
+ `vault:secrets:<ns>` y un `cn` que el acta reconoce, y al arrancar piden su bundle.
549
+ Esto es la cara Node del paquete hermano **`@dotrino/vault`**; la documentación
550
+ completa está en [`lib/README.md`](./lib/README.md).
241
551
 
242
- Son **dos momentos distintos, a propósito**: el **enrolamiento** (registro de la
243
- máquina) es un **comando previo** que corre un humano **una sola vez**; el
244
- **arranque** de la app solo lee la identidad ya guardada y no interactúa con nadie.
245
-
246
- #### 1) Enrolamiento — comando previo, una vez por máquina
552
+ Son **dos momentos distintos, a propósito**: el **enrolamiento** es un comando previo
553
+ que corre un humano **una sola vez** por máquina; el **arranque** solo lee la
554
+ identidad ya guardada y no interactúa con nadie.
247
555
 
248
556
  ```bash
249
557
  # en el VAULT (tu PC)
@@ -251,92 +559,130 @@ dotrino-vault pair --service proxy # invitación con scope SOLO
251
559
  dotrino-vault secret set proxy TURN_KEY_ID …
252
560
 
253
561
  # en la MÁQUINA del servicio (pega la invitación; te MUESTRA un código)
254
- npx dotrino-env enroll --ns proxy
562
+ npx -p @dotrino/vault dotrino-env enroll --ns proxy # el bin vive en @dotrino/vault
255
563
 
256
- # de vuelta en el VAULT: tipeas el código LEYÉNDOLO de la pantalla del servicio
257
- dotrino-vault approve 7K3F-92Q1
564
+ # de vuelta en el VAULT: tecleas los 6 dígitos LEYÉNDOLOS de la pantalla del servicio
565
+ dotrino-vault approve 418027
258
566
  ```
259
567
 
260
568
  Deja `~/.dotrino/service/<ns>/service-identity.json` (0600) con la llave del
261
- dispositivo (generada ahí, nunca sale) + el cert. **No hay ningún secreto en disco.**
569
+ dispositivo (generada ahí, nunca sale) + el cert. **En la máquina del servicio no
570
+ queda ningún secreto**: los valores viven solo en memoria del proceso. En la
571
+ **bóveda** sí quedan en disco (`secrets.json`, 0600, en claro). Un agente tiene **una
572
+ sola** identidad y se la cede el vault: volver a enrolar **reemplaza** la anterior, que
573
+ es la forma de rotar la de un agente comprometido.
262
574
 
263
575
  **Por qué NO se enrola en el primer arranque de la app:** el enrolamiento exige un
264
576
  humano que **lea el código en la pantalla del servicio** — es lo único que impide que
265
- un vault falso (que nunca vio el código) enrole la máquina, o que alguien apruebe a
266
- ciegas. Un servicio arranca bajo systemd/PM2, sin TTY y sin nadie mirando: el código
267
- acabaría en un log (y quien lea el log ya podría aprobar). Además el arranque quedaría
268
- bloqueado esperando una aprobación que quizá nadie da, y un reinicio automático de
269
- madrugada intentaría re-enrolar. Separados, el arranque es determinista e idempotente:
270
- solo **lee**; el enrolamiento **escribe** y consume una invitación de un solo uso.
271
-
272
- Va donde va el `npm ci` al aprovisionar el VPS. (Para máquinas efímeras —Docker,
273
- autoescalado— haría falta una invitación pre-provisionada de un solo uso con TTL corto
274
- y auto-aprobación; **aún no está decidido ni implementado**.)
275
-
276
- #### 2) Arranque — sin interacción, en cada reinicio
577
+ una bóveda falsa (que nunca vio el código) enrole la máquina. Un servicio arranca bajo
578
+ systemd/PM2, sin TTY y sin nadie mirando: el código acabaría en un log. Separados, el
579
+ arranque es determinista e idempotente: solo **lee**; el enrolamiento **escribe** y
580
+ consume una invitación de un solo uso.
277
581
 
278
582
  ```js
279
- import '@dotrino/vault/config' // como `dotenv/config`, pero contra el vault (ns = DOTRINO_NS)
583
+ import '@dotrino/vault/config' // como `dotenv/config`, pero contra el vault
584
+ // ns: DOTRINO_NS, o el único enrolado en la máquina
280
585
  console.log(process.env.TURN_KEY_ID)
281
586
  ```
282
587
 
283
- o explícito, con `@dotrino/vault/env` / `@dotrino/vault/service`:
284
-
285
- ```js
286
- import { loadEnv } from '@dotrino/vault/env'
287
- const { secrets } = await loadEnv({ ns: 'proxy', required: ['TURN_KEY_ID'] })
288
- ```
289
-
290
- **Modos de fallo (importante):**
291
-
292
- - **Vault caído / proxy caído** **espera** (reintento con backoff, para siempre). Sin
293
- vault el servicio no opera: no arranca con secretos viejos ni vacíos.
294
- - **Sin enrolar, cert revocado o vencido, scope equivocado** **aborta en el acto**.
295
- Son errores que no se arreglan reintentando: hay que (re)enrolar.
296
-
297
- CLI de apoyo: `dotrino-env status` (qué hay enrolado aquí), `dotrino-env check` (lista
298
- los **nombres** de los secretos, nunca los valores), `dotrino-env run -- <cmd>` (inyecta
299
- los secretos en el entorno de un proceso que no es Node).
300
-
301
- Garantías: la petición va firmada por la llave del servicio + cert (scope solo
302
- su `ns`); la respuesta viaja **sellada** a una llave efímera por petición (el
303
- proxy que la transporta no puede leerla, y un replay no se puede descifrar) y
304
- **firmada por la maestra** (verificada contra la `iss` pineada del enrolamiento).
305
- Revocar el cert (`revoke`) corta el acceso de inmediato; `activity` audita cada
306
- lectura. Los servicios críticos sin secretos en su core (el propio proxy) arrancan
307
- sin vault; solo la feature que los necesita (TURN) espera. Primer consumidor:
308
- `dotrino-proxy` (TURN de Cloudflare, ver su README).
588
+ **Precedencia el vault MANDA** (desde `@dotrino/vault` 0.14.0): lo que venga del
589
+ vault **pisa** el `.env` y el entorno. No lo reemplaza (el `.env` sigue arrancando
590
+ cualquier máquina sin enrolar), pero tiene la última palabra sobre las claves que
591
+ administra. Es lo que hace barata la **rotación**: se cambia en un solo lugar y ningún
592
+ `.env` rancio olvidado en un VPS puede seguir ganando.
593
+
594
+ **Al rotar, el agente SE REINICIA.** Cuando guardas o borras un secreto, la bóveda
595
+ manda a los agentes de ese `ns` un aviso **firmado** —sin valores— y agrupa las
596
+ escrituras seguidas para que cargar cinco variables no provoque cinco reinicios. El
597
+ agente **no recarga en caliente: termina**, y lo levanta su supervisor. Así que **el
598
+ servicio tiene que correr bajo pm2 o systemd con `Restart=always`**: sin supervisor, la
599
+ primera rotación lo deja apagado. Sale en vez de recargar porque en JavaScript un
600
+ secreto no se puede borrar de la memoria (los strings son inmutables, no hay
601
+ `zeroize`) y una llave se rota casi siempre *porque se filtró*: un proceso nuevo
602
+ empieza con el heap limpio.
603
+
604
+ **Modos de fallo:**
605
+
606
+ - **Vault caído / proxy caído** **espera** (reintento con backoff, para siempre).
607
+ Esperar al vault es la **regla**: arrancar igual sería operar con la configuración
608
+ vieja del `.env`, que es justo lo que el vault vino a dejar de ser. La **única**
609
+ excepción es el **proxio**, y no por importancia sino por estructura: el vault habla
610
+ con sus servicios *por* el proxio, así que un proxio que lo espera espera a alguien
611
+ que necesita el proxio escuchando. Para ese caso está `applyEnv`.
612
+ - **Sin enrolar, cert revocado o vencido, scope equivocado, o un acta que no reconoce
613
+ a ese miembro como el servicio `<ns>`** → **aborta en el acto**. No se arreglan
614
+ reintentando: hay que (re)enrolar. El cert del servicio vive 30 días y se renueva
615
+ **al pedir los secretos** —o sea, al arrancar— cuando le quedan menos de 7: no hay
616
+ temporizador. Así que «vencido» pasa tanto si el agente estuvo apagado más de un mes
617
+ como si estuvo encendido más de un mes sin reiniciarse.
618
+
619
+ Revocar el cert corta el acceso: la bóveda emite un `REVOKED` **firmado** y el agente
620
+ que está a la escucha se apaga en el acto y no vuelve. Un agente sin escucha
621
+ (`DOTRINO_ENV_WATCH=0`, o `applyEnv` a secas) conserva en memoria lo que ya había leído
622
+ hasta que alguien lo reinicie.
623
+
624
+ | Variable | Para qué |
625
+ |---|---|
626
+ | `DOTRINO_NS` | namespace de secretos; si falta, el único enrolado en la máquina |
627
+ | `DOTRINO_ENV_OVERRIDE=0` | por esta corrida, gana el entorno (el vault deja de pisar) |
628
+ | `DOTRINO_ENV_WATCH=0` | no escuchar avisos de cambio (el proceso no termina al rotar) |
629
+ | `DOTRINO_ENV_DIR` | dónde está `service-identity.json` de ese ns |
630
+ | `DOTRINO_ENV_HOME` | raíz de las identidades de servicio (por defecto `~/.dotrino/service`) |
631
+ | `DOTRINO_ENV_QUIET=1` | no imprimir la línea de arranque |
632
+
633
+ CLI de apoyo: `dotrino-env status` (qué hay enrolado aquí), `dotrino-env check` (los
634
+ **nombres** de los secretos, nunca los valores), `dotrino-env run -- <cmd>` (inyecta
635
+ los secretos en el entorno de un proceso que no es Node). Primer consumidor:
636
+ `dotrino-proxy` (TURN de Cloudflare).
309
637
 
310
638
  ## Alcance
311
639
 
312
- - **v1 (este):** servicio headless en Node (Linux), **multi-perfil**, con
313
- **contraseña opcional por perfil para editarlo** (verificador PBKDF2) pero la
314
- **clave privada en claro** en el disco (en `~/.local/share/dotrino/vault`, permisos
315
- `0600`). Enrolamiento de dispositivos, firma delegada y lectura del árbol de
316
- contenidos por el proxy. Distribución como binario único (Node SEA) + servicio systemd.
317
- - **v2:** **cifrado en reposo** con la contraseña (keychain del SO o archivo, a
318
- elección) hoy la contraseña es un candado de edición, no cifra la clave; UI de
319
- escritorio (Tauri) como cliente del daemon; firma de documentos con sellado de
320
- tiempo (`dotrino-signer`); macOS y Windows.
640
+ - **v1 (este):** daemon headless en Node, **multi-perfil**. En **Linux** queda como
641
+ servicio `systemd --user` (binario único Node SEA: `.deb` y tarball); en **Windows y
642
+ macOS** se corre con `npx`, en primer plano; y en cualquier sistema, con **Docker**
643
+ (imagen amd64/arm64 en GHCR). Identidad **cifrada en reposo y ligada a esta
644
+ máquina**. Emparejamiento endurecido con los dos caminos de vinculación, acta del
645
+ perfil (miembros, capacidades y `cn` de servicio), firma delegada, renovación
646
+ automática del cert a los 30 días, store de hilos/aperturas/perfil **cifrado de punta
647
+ a punta**, secretos de servicios sellados a una llave efímera, y bitácora de
648
+ actividad. Contraseña opcional por perfil, **para editarlo** (verificador PBKDF2).
649
+ - **v2:** cifrado en reposo **con la contraseña del perfil** (hoy la clave sale solo de
650
+ la máquina) y atado a tu cuenta del sistema —DPAPI en Windows, Keychain en macOS— o
651
+ al TPM; cifrar también los secretos y el store; **instalador de un clic para Windows
652
+ y macOS**; UI de escritorio (Tauri) como cliente del daemon; firma de documentos con
653
+ sellado de tiempo (`dotrino-signer`).
321
654
 
322
655
  ## Estructura
323
656
 
324
- - `src/vault.js` — núcleo de UN perfil (Identity + transporte + router: enrolar/firmar/leer).
657
+ - `src/vault.js` — núcleo de UN perfil: identidad + transporte + el router de mensajes.
325
658
  - `src/profiles.js` — registro multi-perfil (`profiles.json`, `p/<id>/`) + candado por contraseña.
326
- - `src/manager.js` — corre todos los perfiles a la vez (uno por maestra/conexión).
327
- - `src/daemon.js` — modo servicio: `state.json`, emparejamiento por señal, apagado limpio.
659
+ - `src/manager.js` — corre todos los perfiles a la vez (uno por llave/conexión) y aplica el freno de borrado.
660
+ - `src/daemon.js` — modo servicio: `state.json`, control por archivos + señales, apagado limpio.
328
661
  - `src/ctl.js` — CLI de control (habla con el daemon por archivos + señales, sin socket).
329
662
  - `src/vaultControl.js` — API de control programática (misma vía que la CLI); la usa la TUI.
330
- - `src/tui/` — interfaz de terminal a pantalla completa, sin dependencias (`term.js` primitivas ANSI/raw-mode; `app.js` pantallas).
331
- - `src/transport.js` — conexión headless al proxy + `identify` firmado.
332
- - `src/store.js` — árbol de contenidos (`vault.json`, versionado).
333
- - `src/client.js` — helper de **dispositivo** (enrolar / pedir firma / leer).
334
- - `src/protocol.js` — tipos de mensaje y scopes. · `src/qr.js` — QR ASCII. · `src/paths.js` — dirs.
335
- - `lib/src/invite.js` — la invitación de emparejamiento: cómo se comprime y cómo se lee.
663
+ - `src/tui/` — interfaz de terminal, sin dependencias (`term.js` primitivas ANSI/raw-mode; `app.js` pantallas; `i18n.js` los textos es/en).
664
+ - `src/transport.js` — conexión headless al proxy + `identify` firmado (con el acta).
665
+ - `src/threadStore.js` — el store del usuario: hilos, "recientes" y perfil (`threads.json`).
666
+ - `src/store.js` — árbol de contenidos (`vault.json`, versionado). Hoy es un esqueleto: se lee, nadie escribe.
667
+ - `src/secretsStore.js` — secretos por namespace de servicio (`secrets.json`), validados.
668
+ - `src/atrest.js` — cifrado en reposo de la identidad, ligado a esta máquina.
669
+ - `src/client.js` — helper de **dispositivo** (enrolar / pedir firma / leer), para pruebas.
670
+ - `src/version.js` — de dónde sale la versión (binario SEA / npm / repo), para que `status` avise si el daemon quedó viejo.
671
+ - `src/node-globals.js` — los globals de navegador que los paquetes del ecosistema esperan al correr headless.
672
+ - `src/protocol.js` — re-export de `lib/src/protocol.js`, la **fuente única** de tipos de mensaje y scopes.
673
+ - `src/qr.js` — QR ASCII. · `src/paths.js` — dirs de datos por sistema.
674
+ - `lib/` — el paquete npm **`@dotrino/vault`** (bóveda en el navegador + cliente de servicio `dotrino-env`), versionado y publicado **aparte** de la raíz. Dentro: `enroll.js` (el lado-bóveda del emparejamiento, compartido), `invite.js` (la invitación), `service.js`/`env.js`/`config.js`, `sealed.js`, `protocol.js`.
336
675
  - `bin/sea-entry.js` — entrypoint del binario único (multicall daemon / `--ctl` / `--tui`).
337
- - `bin/dotrino-vaultd.js` — entrypoint de desarrollo (node directo).
338
- - `bin/dotrino-vault-tui.js` — entrypoint de desarrollo de la TUI.
339
- - `packaging/` — `build.sh` (binario), `install.sh`/`uninstall.sh`, unit systemd.
340
- - `web/` — la página `vault.dotrino.com` (Vite + Vue).
676
+ - `bin/dotrino-vault.js` — el CLI de control instalado por npm. · `bin/dotrino-vaultd.js` y `bin/dotrino-vault-tui.js` — entrypoints de desarrollo.
677
+ - `vendor/qrcode-generator.cjs` — el encoder de QR vendorizado (MIT): nada de JS de terceros en runtime.
678
+ - `packaging/` — `build.sh` (binario SEA + tarball), `build-deb.sh`, `build-win.sh`, `install.sh`/`uninstall.sh`, unit systemd.
679
+ - `Dockerfile` — la imagen que publica `.github/workflows/docker.yml` en GHCR.
680
+ - `test/` — las pruebas (`npm test`, `node --test`, sin dependencias).
681
+ - `web/` — `vault.dotrino.com` (Vite + Vue): la página pública **y la consola «Dónde vive tu perfil»**, la única pantalla del ecosistema donde se ven y gestionan los dispositivos de un perfil. Sirve además las rutas del QR (`/d` y `/dispositivos`). La publica `.github/workflows/deploy.yml`.
682
+ - `docs/` — las decisiones de diseño, que mandan sobre el código:
683
+ - [`acta-de-perfil.md`](./docs/acta-de-perfil.md) — el modelo vigente: un perfil es un conjunto de llaves con un acta firmada por un solo sellador.
684
+ - [`pairing-protocol.md`](./docs/pairing-protocol.md) — el emparejamiento endurecido: por qué el token dejó de ser autoridad suficiente.
685
+ - [`vinculacion-de-cuentas.md`](./docs/vinculacion-de-cuentas.md) — los dos caminos al conectar un aparato, y por qué no existe fusionar cuentas.
686
+ - [`store-identity-architecture.md`](./docs/store-identity-architecture.md) — por qué la bóveda del PC hace de store además de identidad.
341
687
 
342
688
  Sin anuncios, sin cuentas, sin rastreo. MIT · parte de Dotrino.