@dotrino/vault 0.15.0 → 0.16.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 +61 -0
- package/package.json +1 -1
- package/src/config.js +21 -1
- package/src/env.js +74 -5
- package/src/protocol.js +14 -0
- package/src/service.js +137 -0
package/README.md
CHANGED
|
@@ -91,6 +91,25 @@ que los secretos estén. Si el vault no está disponible, **espera** (reintento
|
|
|
91
91
|
backoff) — un servicio sin vault no arranca, no opera con secretos viejos ni vacíos.
|
|
92
92
|
Un fallo NO transitorio (sin enrolar, cert revocado, scope equivocado) sí aborta.
|
|
93
93
|
|
|
94
|
+
#### Esperar al vault es la REGLA. La excepción es una sola
|
|
95
|
+
|
|
96
|
+
Un agente enrolado **espera**. No es una preferencia: arrancar igual significaría
|
|
97
|
+
operar con la configuración vieja del `.env`, que es justo lo que el vault vino a
|
|
98
|
+
dejar de ser. Y la espera casi nunca duele, porque estos agentes no son críticos:
|
|
99
|
+
que un bot o un firmador tarden en levantar no rompe a nadie.
|
|
100
|
+
|
|
101
|
+
La **única** excepción conocida es **el proxio**, y no por importancia sino por una
|
|
102
|
+
razón estructural: el vault habla con sus servicios **por el proxio**. Un proxio que
|
|
103
|
+
espera al vault espera a alguien que necesita que el proxio ya esté escuchando —
|
|
104
|
+
abrazo mortal, y con él se cae el vault de todo el mundo. Por eso el proxio arranca
|
|
105
|
+
con lo que tenga y aplica la configuración cuando llega, con `applyEnv`.
|
|
106
|
+
|
|
107
|
+
> `applyEnv` existe **para ese caso**, no como alternativa cómoda al bloqueo. Si tu
|
|
108
|
+
> agente no está en el camino por el que viaja el propio vault, usa
|
|
109
|
+
> `import '@dotrino/vault/config'` y deja que espere. El precio de la excepción es
|
|
110
|
+
> real: lo que sólo se lee al arrancar llega tarde y no toma efecto hasta reiniciar,
|
|
111
|
+
> así que hay que avisarlo en el log — el proxio lo hace.
|
|
112
|
+
|
|
94
113
|
Para procesos que no son Node, el CLI los inyecta en el entorno de un hijo:
|
|
95
114
|
|
|
96
115
|
```bash
|
|
@@ -120,6 +139,48 @@ suya hacia arriba. De ahí tres reglas, que el paquete aplica solas:
|
|
|
120
139
|
> instancias y citas vivas dejan de resolver y los peers que lo tenían pineado lo
|
|
121
140
|
> rechazan hasta re-pinearlo. Es a propósito: así se echa a un nodo comprometido.
|
|
122
141
|
|
|
142
|
+
### Rotar: la bóveda avisa y el agente SE REINICIA
|
|
143
|
+
|
|
144
|
+
Cambiar un secreto en la bóveda no sirve de nada si quien lo usa no se entera. Al
|
|
145
|
+
guardar, la bóveda manda un **aviso firmado** a los agentes de ese `ns` (sin
|
|
146
|
+
valores: sólo dice que cambió), **agrupando** las escrituras seguidas para que
|
|
147
|
+
cargar cinco valores no provoque cinco reinicios.
|
|
148
|
+
|
|
149
|
+
El agente **no recarga en caliente: termina**, y lo levanta su supervisor (pm2,
|
|
150
|
+
systemd `Restart=always`). Con `import '@dotrino/vault/config'` ya viene puesto.
|
|
151
|
+
|
|
152
|
+
Salir en vez de recargar, por tres razones — y la primera es la de peso:
|
|
153
|
+
|
|
154
|
+
1. **Borra de memoria el valor viejo.** En JavaScript un secreto no se puede
|
|
155
|
+
borrar: los strings son inmutables, no hay `zeroize`, y el valor sigue en el
|
|
156
|
+
heap hasta que al recolector le apetezca, más lo que capturó cada *closure* y
|
|
157
|
+
cada caché derivada. Una llave se rota casi siempre **porque se filtró**, así
|
|
158
|
+
que dejarla viva en el proceso anula el motivo de rotarla. Un proceso nuevo
|
|
159
|
+
empieza con el heap limpio.
|
|
160
|
+
2. **Lee todo fresco.** Recargar en caliente exige que cada sitio que leyó una
|
|
161
|
+
variable sepa releerla; esa lista hay que mantenerla para siempre y, cuando se
|
|
162
|
+
queda corta, falla en silencio.
|
|
163
|
+
3. **Es un interruptor de emergencia.** Revocar el cert de un agente ya no espera a
|
|
164
|
+
que alguien se acuerde de reiniciarlo: recibe el `REVOKED` firmado, se apaga, y
|
|
165
|
+
al arrancar `fetchSecrets` recibe «no autorizado: revoked», que no se arregla
|
|
166
|
+
reintentando. Antes, revocar no le quitaba nada a un proceso ya corriendo.
|
|
167
|
+
|
|
168
|
+
Defensas, porque una señal que provoca reinicios es un arma si se descuida: firma
|
|
169
|
+
de la maestra pineada, `ns` que coincida, frescura y anti-replay, **gracia de
|
|
170
|
+
arranque** y **piso entre avisos** (si la configuración nueva rompe el arranque, sin
|
|
171
|
+
eso el servicio entra en ciclo) y **jitter** (diez agentes del mismo `ns` no salen
|
|
172
|
+
todos en el mismo segundo).
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
import { watchEnv } from '@dotrino/vault/env'
|
|
176
|
+
await watchEnv({ ns: 'miapp' }) // termina el proceso al cambiar
|
|
177
|
+
await watchEnv({ ns: 'proxy', onUpdate: (i) => … }) // o decide tú (ver abajo)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`onUpdate` es para cuando terminar **no es una opción**. El caso real es el proxio:
|
|
181
|
+
reiniciarlo corta el transporte de todo el ecosistema, así que anota el aviso, lo
|
|
182
|
+
publica en su `GET /peers` y deja el momento a un humano.
|
|
183
|
+
|
|
123
184
|
### Precedencia: el vault MANDA
|
|
124
185
|
|
|
125
186
|
Los valores del vault **pisan** los del `.env` y los del entorno. El vault no
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Usa ESTE dispositivo (navegador) como bóveda/CA del ecosistema Dotrino: atiende enrolamientos por el proxy y firma certificados de delegación a tus máquinas. Incluye el cliente de SERVICIO (Node): un proyecto se enrola una vez y jala sus credenciales del vault en vez del .env (`import '@dotrino/vault/config'`).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/config.js
CHANGED
|
@@ -11,13 +11,21 @@
|
|
|
11
11
|
* EL VAULT MANDA: lo que venga de aquí PISA lo que ya hubiera en el entorno
|
|
12
12
|
* (incluido un `.env` cargado antes). Ver `env.js` para el porqué.
|
|
13
13
|
*
|
|
14
|
+
* Y queda A LA ESCUCHA: cuando el dueño cambia la configuración en la bóveda, el
|
|
15
|
+
* proceso TERMINA para que su supervisor lo levante limpio. No se recarga en
|
|
16
|
+
* caliente a propósito — salir borra de la memoria el valor viejo, que en
|
|
17
|
+
* JavaScript no hay forma de borrar de otra manera, y una llave se rota casi
|
|
18
|
+
* siempre porque se filtró. Requiere correr bajo pm2 o systemd `Restart=always`.
|
|
19
|
+
* Se apaga con `DOTRINO_ENV_WATCH=0`.
|
|
20
|
+
*
|
|
14
21
|
* Config por entorno:
|
|
15
22
|
* DOTRINO_NS namespace de secretos (si no, el único enrolado en la máquina)
|
|
16
23
|
* DOTRINO_ENV_DIR directorio de la identidad del servicio (si no, ~/.dotrino/service/<ns>)
|
|
17
24
|
* DOTRINO_ENV_QUIET '1' para no imprimir la línea de arranque
|
|
18
25
|
* DOTRINO_ENV_OVERRIDE '0' para NO pisar el entorno en esta corrida (depuración)
|
|
26
|
+
* DOTRINO_ENV_WATCH '0' para no escuchar avisos de cambio (no terminar solo)
|
|
19
27
|
*/
|
|
20
|
-
import { loadEnv } from './env.js'
|
|
28
|
+
import { loadEnv, watchEnv } from './env.js'
|
|
21
29
|
|
|
22
30
|
const quiet = process.env.DOTRINO_ENV_QUIET === '1'
|
|
23
31
|
|
|
@@ -36,3 +44,15 @@ if (!quiet) {
|
|
|
36
44
|
console.error('[dotrino-env] pisaron un valor previo del entorno: %s', overridden.join(', '))
|
|
37
45
|
}
|
|
38
46
|
}
|
|
47
|
+
|
|
48
|
+
// La escucha no debe impedir que el proceso termine por su cuenta si no tiene
|
|
49
|
+
// nada más que hacer, así que si falla (agente sin enrolar del todo, proxio
|
|
50
|
+
// caído) se avisa y se sigue: el servicio ya tiene su configuración; lo único
|
|
51
|
+
// que pierde es enterarse del próximo cambio.
|
|
52
|
+
if (process.env.DOTRINO_ENV_WATCH !== '0') {
|
|
53
|
+
try {
|
|
54
|
+
await watchEnv({ ns, quiet })
|
|
55
|
+
} catch (e) {
|
|
56
|
+
if (!quiet) console.error('[dotrino-env] sin escucha de cambios (%s): habrá que reiniciar a mano al rotar', e.message)
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/env.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import fs from 'node:fs'
|
|
22
22
|
import os from 'node:os'
|
|
23
23
|
import path from 'node:path'
|
|
24
|
-
import { fetchSecrets, waitForSecrets, readServiceIdentity } from './service.js'
|
|
24
|
+
import { fetchSecrets, waitForSecrets, readServiceIdentity, watchSecretsChanges } from './service.js'
|
|
25
25
|
import { isValidSecretsNs } from './protocol.js'
|
|
26
26
|
|
|
27
27
|
/** Raíz donde viven las identidades de servicio de esta máquina/usuario. */
|
|
@@ -113,10 +113,19 @@ export async function loadEnv ({ ns, dir, override, wait = true, required = [],
|
|
|
113
113
|
/**
|
|
114
114
|
* Vuelca un bundle de secretos en `process.env` y cuenta qué cambió.
|
|
115
115
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
116
|
+
* ⚠ ESTO NO ES LA FORMA NORMAL. Un agente enrolado ESPERA al vault
|
|
117
|
+
* (`loadEnv` / `import '@dotrino/vault/config'`): arrancar igual sería operar con
|
|
118
|
+
* la configuración vieja del `.env`, que es justo lo que el vault vino a dejar de
|
|
119
|
+
* ser, y la espera casi nunca duele porque estos agentes no son críticos.
|
|
120
|
+
*
|
|
121
|
+
* `applyEnv` existe para la ÚNICA excepción estructural: **el proxio**. El vault
|
|
122
|
+
* habla con sus servicios POR el proxio, así que un proxio que espera al vault
|
|
123
|
+
* espera a alguien que necesita el proxio escuchando — abrazo mortal, y con él se
|
|
124
|
+
* cae el vault de todos. Ese caso arranca con lo que tenga y aplica el bundle
|
|
125
|
+
* cuando llega, tarde y por su cuenta.
|
|
126
|
+
*
|
|
127
|
+
* Si tu agente no está en el camino por el que viaja el propio vault, no uses
|
|
128
|
+
* esto: usa `loadEnv` y deja que espere.
|
|
120
129
|
*
|
|
121
130
|
* @returns {{injected:string[], overridden:string[], skipped:string[]}}
|
|
122
131
|
*/
|
|
@@ -135,3 +144,63 @@ export function applyEnv (secrets, override = overrideByDefault()) {
|
|
|
135
144
|
}
|
|
136
145
|
return { injected, overridden, skipped }
|
|
137
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Queda a la escucha de la bóveda y, cuando avisa que la configuración cambió,
|
|
150
|
+
* TERMINA EL PROCESO para que el supervisor lo levante con todo fresco.
|
|
151
|
+
*
|
|
152
|
+
* Por qué salir en vez de recargar en caliente:
|
|
153
|
+
*
|
|
154
|
+
* 1. **Borra de memoria el valor viejo.** Es la razón de peso. En JavaScript un
|
|
155
|
+
* secreto no se puede borrar: los strings son inmutables, no hay `zeroize`, y
|
|
156
|
+
* el valor sigue en el heap hasta que al recolector le apetezca — más lo que
|
|
157
|
+
* capturó cada closure y cada caché derivada. Y una llave se rota casi siempre
|
|
158
|
+
* PORQUE SE FILTRÓ, así que dejarla viva en el proceso anula el motivo de
|
|
159
|
+
* rotarla. Un proceso nuevo empieza con el heap limpio.
|
|
160
|
+
* 2. **Lee todo fresco.** Recargar en caliente exige que cada sitio que leyó una
|
|
161
|
+
* variable sepa releerla; esa lista hay que mantenerla para siempre y cuando se
|
|
162
|
+
* queda corta falla en silencio.
|
|
163
|
+
* 3. **Sirve de interruptor de emergencia.** Revocar el cert de un agente ya no
|
|
164
|
+
* espera a que alguien se acuerde de reiniciarlo: se apaga, y al arrancar
|
|
165
|
+
* `fetchSecrets` recibe «no autorizado: revoked» y no vuelve.
|
|
166
|
+
*
|
|
167
|
+
* NO se reinicia solo: sale, y lo levanta quien lo supervisa (pm2, systemd con
|
|
168
|
+
* `Restart=always`). Un proceso no puede reiniciarse a sí mismo de forma fiable, y
|
|
169
|
+
* el supervisor ya trae backoff y tope de intentos, que es justo lo que evita que
|
|
170
|
+
* una configuración rota se convierta en un ciclo.
|
|
171
|
+
*
|
|
172
|
+
* @param {Object} [opts]
|
|
173
|
+
* @param {string} [opts.ns]
|
|
174
|
+
* @param {string} [opts.dir]
|
|
175
|
+
* @param {(info:{ns:string, ts:number, motivo:'cambio'|'revocado'})=>void} [opts.onUpdate]
|
|
176
|
+
* Reemplaza la salida por defecto. Úsalo cuando terminar el proceso no sea una
|
|
177
|
+
* opción — el caso del proxio, cuyo reinicio corta el transporte de todos.
|
|
178
|
+
* @param {number} [opts.exitCode=0] Salida LIMPIA: systemd con `Restart=on-failure`
|
|
179
|
+
* no levantaría un servicio que sale con 0, pero `Restart=always` sí, y pm2
|
|
180
|
+
* también. Se elige 0 porque salir a propósito no es un fallo.
|
|
181
|
+
* @returns {Promise<{stop:()=>void}>}
|
|
182
|
+
*/
|
|
183
|
+
export async function watchEnv ({ ns, dir, onUpdate, exitCode = 0, quiet = false, ...resto } = {}) {
|
|
184
|
+
ns = resolveNs(ns)
|
|
185
|
+
dir = dir || serviceDir(ns)
|
|
186
|
+
const decir = (m) => { if (!quiet) console.error(m) }
|
|
187
|
+
|
|
188
|
+
const salir = (motivo) => {
|
|
189
|
+
decir(`[dotrino-env] ${motivo === 'revocado'
|
|
190
|
+
? 'la bóveda REVOCÓ este agente: terminando (no volverá a arrancar)'
|
|
191
|
+
: 'configuración nueva en la bóveda: terminando para que el supervisor lo levante limpio'}`)
|
|
192
|
+
process.exit(motivo === 'revocado' ? 1 : exitCode)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return watchSecretsChanges({
|
|
196
|
+
dir,
|
|
197
|
+
ns,
|
|
198
|
+
log: decir,
|
|
199
|
+
onChange: ({ ts }) => (onUpdate ? onUpdate({ ns, ts, motivo: 'cambio' }) : salir('cambio')),
|
|
200
|
+
// Un cert revocado sale con código de FALLO a propósito: si el supervisor lo
|
|
201
|
+
// levanta, va a morir otra vez al no poder leer sus secretos, y el contador de
|
|
202
|
+
// reinicios fallidos es lo que hace que se note en vez de girar en silencio.
|
|
203
|
+
onRevoked: () => (onUpdate ? onUpdate({ ns, ts: Date.now(), motivo: 'revocado' }) : salir('revocado')),
|
|
204
|
+
...resto
|
|
205
|
+
})
|
|
206
|
+
}
|
package/src/protocol.js
CHANGED
|
@@ -45,6 +45,20 @@ export const MSG = Object.freeze({
|
|
|
45
45
|
RENEWED: 'vault.renewed', // vault → dispositivo: { cert } (cert fresco, misma sub-clave/scope)
|
|
46
46
|
SECRETS: 'vault.secrets', // servicio → vault: { data:{op,ns,ek,publickey,ts}, signature, cert }
|
|
47
47
|
SECRETS_RESULT: 'vault.secrets.result', // vault → servicio: { body:{op,ns,enc,ts}, signature } (enc SELLADO a ek; body firmado por la maestra)
|
|
48
|
+
// AVISO DE CAMBIO (no lleva valores): la bóveda dice «la configuración del ns
|
|
49
|
+
// cambió». El agente no la recarga en caliente — SALE limpio y su supervisor lo
|
|
50
|
+
// levanta. Dos razones, y la segunda es la de peso:
|
|
51
|
+
// · Lee todo fresco. Recargar en caliente exige que cada sitio que leyó una
|
|
52
|
+
// variable sepa releerla, y esa lista hay que mantenerla para siempre.
|
|
53
|
+
// · BORRA DE MEMORIA EL VALOR VIEJO. En JavaScript un secreto no se puede
|
|
54
|
+
// borrar: los strings son inmutables, no hay zeroize, y el valor queda en el
|
|
55
|
+
// heap hasta que al recolector le apetezca — más lo que capturó cada closure
|
|
56
|
+
// y cada caché derivada. Una llave se rota casi siempre PORQUE SE FILTRÓ, así
|
|
57
|
+
// que dejarla viva en el proceso anula la razón de rotarla. Un proceso nuevo
|
|
58
|
+
// empieza con el heap limpio.
|
|
59
|
+
// Va FIRMADO por la maestra y el agente lo verifica contra su `iss` pineada: un
|
|
60
|
+
// aviso de reinicio sin autenticar ES un ataque de denegación.
|
|
61
|
+
SECRETS_CHANGED: 'vault.secrets.changed', // vault → servicio: { body:{op,ns,ts}, signature }
|
|
48
62
|
ERROR: 'vault.error' // vault → dispositivo: { error }
|
|
49
63
|
})
|
|
50
64
|
|
package/src/service.js
CHANGED
|
@@ -365,6 +365,143 @@ export async function fetchSecrets ({ dir, ns, proxyUrl, masterPubkey, device, c
|
|
|
365
365
|
} finally { client.close() }
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Escucha los avisos de cambio de configuración de la bóveda.
|
|
370
|
+
*
|
|
371
|
+
* A diferencia de `fetchSecrets` —que abre, pide y cierra—, esto mantiene la
|
|
372
|
+
* conexión ABIERTA e identificada con la llave del servicio: es la dirección a la
|
|
373
|
+
* que la bóveda le habla. Por eso un agente que quiera enterarse de una rotación
|
|
374
|
+
* deja de ser un cliente de paso y pasa a ser uno permanente.
|
|
375
|
+
*
|
|
376
|
+
* Lo que NO hace: recargar nada. El aviso no trae valores, y la reacción correcta
|
|
377
|
+
* es que el proceso termine y lo levante su supervisor (ver `watchEnv`).
|
|
378
|
+
*
|
|
379
|
+
* Defensas, porque una señal que provoca reinicios es un arma si se descuida:
|
|
380
|
+
* · **Firma de la maestra pineada** y `ns` que coincida. Sin esto, cualquiera
|
|
381
|
+
* reinicia la flota ajena cuando quiera.
|
|
382
|
+
* · **Frescura y anti-replay**: `ts` dentro de la ventana y estrictamente mayor
|
|
383
|
+
* que el último obedecido. Un aviso viejo reproducido no vuelve a disparar.
|
|
384
|
+
* · **Gracia de arranque y piso entre avisos**: no se obedece recién arrancado ni
|
|
385
|
+
* dos veces seguidas. Si la configuración nueva rompe el arranque, sin esto el
|
|
386
|
+
* servicio entra en un ciclo de reinicios.
|
|
387
|
+
* · **Jitter**: diez agentes del mismo ns no pueden salir todos en el mismo
|
|
388
|
+
* segundo.
|
|
389
|
+
*
|
|
390
|
+
* @param {Object} opts
|
|
391
|
+
* @param {string} opts.dir Identidad del servicio (`service-identity.json`).
|
|
392
|
+
* @param {string} [opts.ns]
|
|
393
|
+
* @param {(info:{ns:string, ts:number})=>void} opts.onChange
|
|
394
|
+
* @param {(info:{nonce:string})=>void} [opts.onRevoked] Cert revocado: apagar YA.
|
|
395
|
+
* @param {number} [opts.graceMs=30000] No obedecer avisos durante los primeros N ms.
|
|
396
|
+
* @param {number} [opts.minIntervalMs=60000] Mínimo entre dos avisos obedecidos.
|
|
397
|
+
* @param {number} [opts.jitterMs=5000] Espera aleatoria antes de avisar.
|
|
398
|
+
* @param {(m:string)=>void} [opts.log]
|
|
399
|
+
* @returns {Promise<{stop:()=>void}>}
|
|
400
|
+
*/
|
|
401
|
+
export async function watchSecretsChanges ({
|
|
402
|
+
dir, ns, onChange, onRevoked, graceMs = 30000, minIntervalMs = 60000, jitterMs = 5000, log = () => {}
|
|
403
|
+
} = {}) {
|
|
404
|
+
const saved = dir ? readServiceIdentity(dir) : null
|
|
405
|
+
ns = ns || saved?.ns
|
|
406
|
+
if (!saved?.device || !saved?.cert || !saved?.iss || !saved?.proxy) {
|
|
407
|
+
throw new Error('servicio sin enrolar: no hay a quién escuchar')
|
|
408
|
+
}
|
|
409
|
+
const master = saved.iss
|
|
410
|
+
const nacido = Date.now()
|
|
411
|
+
let ultimoTs = 0
|
|
412
|
+
let ultimoObedecido = 0
|
|
413
|
+
const enVuelo = new Set() // avisos cuya firma se está comprobando ahora mismo
|
|
414
|
+
let parado = false
|
|
415
|
+
let client = null
|
|
416
|
+
let reintento = null
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* REVOCACIÓN = interruptor de emergencia. Hasta ahora revocar un cert no le
|
|
420
|
+
* quitaba nada a un servicio YA CORRIENDO: seguía operando con los secretos en
|
|
421
|
+
* memoria hasta que alguien se acordara de reiniciarlo (el README decía lo
|
|
422
|
+
* contrario). Teniendo la conexión abierta, el aviso llega y el agente se apaga
|
|
423
|
+
* en el acto — y no vuelve, porque al arrancar `fetchSecrets` recibe
|
|
424
|
+
* «no autorizado: revoked», que no se arregla reintentando.
|
|
425
|
+
*
|
|
426
|
+
* Sin gracia, sin piso y sin jitter, al revés que un cambio de configuración:
|
|
427
|
+
* apagar algo comprometido es justo lo que no debe esperar su turno.
|
|
428
|
+
*/
|
|
429
|
+
const atenderRevocacion = async (payload) => {
|
|
430
|
+
const body = payload.body
|
|
431
|
+
if (!body || body.op !== 'revoke') return
|
|
432
|
+
// Que sea MI revocación y no la de otro dispositivo del mismo dueño.
|
|
433
|
+
if (body.sub !== saved.device.publickey) return
|
|
434
|
+
if (saved.cert?.nonce && body.nonce !== saved.cert.nonce) return
|
|
435
|
+
if (!(await verifyDeviceSig({ publickey: master, data: body, signature: payload.signature }))) {
|
|
436
|
+
return log('[vault] aviso de revocación MAL FIRMADO: ignorado')
|
|
437
|
+
}
|
|
438
|
+
log('[vault] ⚠ la bóveda REVOCÓ el cert de este agente: apagando')
|
|
439
|
+
try { onRevoked?.({ nonce: body.nonce }) } catch (e) { log('[vault] ' + e.message) }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const atender = async (payload) => {
|
|
443
|
+
if (payload?.type === MSG.REVOKED) return atenderRevocacion(payload)
|
|
444
|
+
if (payload?.type !== MSG.SECRETS_CHANGED) return
|
|
445
|
+
const body = payload.body
|
|
446
|
+
if (!body || body.op !== 'secrets.changed' || body.ns !== ns) return
|
|
447
|
+
if (typeof body.ts !== 'number' || Math.abs(Date.now() - body.ts) > FRESH_WINDOW_MS) {
|
|
448
|
+
return log('[vault] aviso de cambio con fecha fuera de ventana: ignorado')
|
|
449
|
+
}
|
|
450
|
+
if (body.ts <= ultimoTs) return log('[vault] aviso de cambio repetido: ignorado')
|
|
451
|
+
// Dos copias del MISMO aviso pueden llegar a la vez, y comprobar la firma es
|
|
452
|
+
// asíncrono: sin esta marca las dos pasarían el corte de `ultimoTs` antes de
|
|
453
|
+
// que ninguna lo actualizara, y el agente se reiniciaría por partida doble.
|
|
454
|
+
// La marca se pone antes del `await` y el `ultimoTs` DESPUÉS de verificar, para
|
|
455
|
+
// que un aviso falso con fecha lejana no pueda dejar fuera a los de verdad.
|
|
456
|
+
if (enVuelo.has(body.ts)) return
|
|
457
|
+
enVuelo.add(body.ts)
|
|
458
|
+
let valida = false
|
|
459
|
+
try {
|
|
460
|
+
valida = await verifyDeviceSig({ publickey: master, data: body, signature: payload.signature })
|
|
461
|
+
} finally { enVuelo.delete(body.ts) }
|
|
462
|
+
if (!valida) return log('[vault] aviso de cambio MAL FIRMADO: ignorado (no viene de tu bóveda)')
|
|
463
|
+
if (body.ts <= ultimoTs) return
|
|
464
|
+
ultimoTs = body.ts
|
|
465
|
+
|
|
466
|
+
const ahora = Date.now()
|
|
467
|
+
if (ahora - nacido < graceMs) {
|
|
468
|
+
return log('[vault] aviso de cambio recién arrancado: ignorado (evita el ciclo de reinicios)')
|
|
469
|
+
}
|
|
470
|
+
if (ahora - ultimoObedecido < minIntervalMs) {
|
|
471
|
+
return log('[vault] aviso de cambio demasiado seguido del anterior: ignorado')
|
|
472
|
+
}
|
|
473
|
+
ultimoObedecido = ahora
|
|
474
|
+
|
|
475
|
+
const espera = Math.floor(Math.random() * jitterMs)
|
|
476
|
+
log(`[vault] la bóveda avisa que la configuración de «${ns}» cambió (en ${espera} ms)`)
|
|
477
|
+
setTimeout(() => { if (!parado) { try { onChange?.({ ns, ts: body.ts }) } catch (e) { log('[vault] ' + e.message) } } }, espera)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const conectar = async () => {
|
|
481
|
+
if (parado) return
|
|
482
|
+
try {
|
|
483
|
+
client = await freshClient(saved.proxy)
|
|
484
|
+
await identifyAsService(client, saved.device)
|
|
485
|
+
client.on('message', (_from, p) => { atender(p).catch(() => {}) })
|
|
486
|
+
// Reconectar solo: si se cae el proxio, el agente deja de ser avisable, y
|
|
487
|
+
// eso es exactamente el momento en que uno querría enterarse de una rotación.
|
|
488
|
+
client.on('disconnected', () => { if (!parado) reintento = setTimeout(conectar, 5000) })
|
|
489
|
+
log('[vault] a la escucha de cambios de configuración')
|
|
490
|
+
} catch (e) {
|
|
491
|
+
if (!parado) reintento = setTimeout(conectar, 5000)
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
await conectar()
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
stop () {
|
|
498
|
+
parado = true
|
|
499
|
+
clearTimeout(reintento)
|
|
500
|
+
try { client?.close() } catch (_) {}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
368
505
|
/**
|
|
369
506
|
* Bucle de arranque de un servicio: pide los secretos y, si el vault no está
|
|
370
507
|
* disponible, REINTENTA para siempre (con backoff hasta `maxRetryMs`). El
|