@dotrino/vaultd 0.112.0 → 0.113.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/package.json +1 -1
- package/src/ctl.js +7 -1
- package/src/manager.js +4 -1
- package/src/transport.js +54 -6
- package/src/vault.js +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.113.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Certificador personal de Dotrino: daemon headless que custodia la clave maestra y delega capacidades a tus dispositivos por el proxy. Tu CA propia.",
|
|
6
6
|
"bin": {
|
package/src/ctl.js
CHANGED
|
@@ -155,7 +155,13 @@ function cmdStatus () {
|
|
|
155
155
|
/** Una línea por perfil: nombre, id, huella y estado del candado. */
|
|
156
156
|
function describeProfile (p) {
|
|
157
157
|
const lock = !p.protected ? 'sin contraseña' : (p.locked ? `${B}🔒 bloqueado${Z}` : '🔓 desbloqueado')
|
|
158
|
-
|
|
158
|
+
// ALCANZABLE, Y SE DICE AQUÍ. Una bóveda puede estar corriendo y fuera de la red —si el
|
|
159
|
+
// proxio le rechazó el `identify`, nadie la alcanza por su pubkey— y desde fuera eso se ve
|
|
160
|
+
// igual que «no hay nada que aprobar»: el teléfono no timbra y no hay nada que mirar.
|
|
161
|
+
// `online` no viaja en las fotos de un daemon anterior, así que solo se enseña cuando el
|
|
162
|
+
// dato está y dice que no.
|
|
163
|
+
const red = p.online === false ? ` ${B}⚠ sin proxio (nadie puede alcanzarla)${Z}` : ''
|
|
164
|
+
return `${B}${p.name || '(sin nombre)'}${Z} ${p.id} ${p.fingerprint || '—'} ${lock}${red}`
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
function showChallenge (pe) {
|
package/src/manager.js
CHANGED
|
@@ -195,7 +195,10 @@ export async function startVaultManager ({ root = dataDir(), proxyUrl, log = con
|
|
|
195
195
|
/** Resumen para state.json / `profile ls`: identidad + candado de cada perfil. */
|
|
196
196
|
const summary = () => profiles.list().map((p) => {
|
|
197
197
|
const v = running.get(p.id)
|
|
198
|
-
|
|
198
|
+
// `online` es ALCANZABLE, no «corriendo»: la bóveda puede estar abierta y con el socket
|
|
199
|
+
// en pie y aun así fuera de la red, si el proxio le rechazó el `identify` (un reloj
|
|
200
|
+
// atrasado al arrancar, p. ej.). Sin esto, eso solo se veía en una línea del log.
|
|
201
|
+
return { ...p, fingerprint: v?.fingerprint || null, iss: v?.master || null, online: v ? !!v.isIdentified?.() : false }
|
|
199
202
|
})
|
|
200
203
|
|
|
201
204
|
return {
|
package/src/transport.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function masterPubkeyOf (identity) {
|
|
|
36
36
|
* @param {string} [opts.url] URL del proxy (default wss://proxy.dotrino.com).
|
|
37
37
|
* @returns {Promise<{ client, token:string, identify():Promise<void> }>}
|
|
38
38
|
*/
|
|
39
|
-
export async function createTransport ({ identity, dir, url = DEFAULT_PROXY, commKey = null, log = () => {} }) {
|
|
39
|
+
export async function createTransport ({ identity, dir, url = DEFAULT_PROXY, commKey = null, log = () => {}, makeClient = null }) {
|
|
40
40
|
installNodeGlobals(dir)
|
|
41
41
|
// Import dinámico DESPUÉS de instalar los globals que el paquete usa.
|
|
42
42
|
// `WebSocketProxyClient` (la clase) y NO el helper `getWebSocketProxyClient`:
|
|
@@ -44,7 +44,11 @@ export async function createTransport ({ identity, dir, url = DEFAULT_PROXY, com
|
|
|
44
44
|
// conexión POR PERFIL (cada maestra se identifica con su propia pubkey ante el
|
|
45
45
|
// proxy). Con el singleton, el segundo perfil reusaba el cliente del primero y
|
|
46
46
|
// su `identify` pisaba al anterior. Sigue siendo el cliente oficial del paquete.
|
|
47
|
-
|
|
47
|
+
// `makeClient` es la costura de las PRUEBAS, y existe por una razón concreta: comprobar
|
|
48
|
+
// que un `identify` rechazado no tumba el perfil y se reintenta solo. Sin ella habría que
|
|
49
|
+
// levantar un proxio de verdad para probar el caso que ya costó una tarde. En producción
|
|
50
|
+
// no se pasa nunca y esto es el cliente oficial de siempre.
|
|
51
|
+
const { WebSocketProxyClient } = makeClient ? {} : await import('@dotrino/proxy-client')
|
|
48
52
|
|
|
49
53
|
/**
|
|
50
54
|
* EL CANAL DIRECTO, ENCENDIDO Y CON LA PUERTA CERRADA.
|
|
@@ -65,13 +69,17 @@ export async function createTransport ({ identity, dir, url = DEFAULT_PROXY, com
|
|
|
65
69
|
// el acta es asíncrono. Así que se guarda una copia y se refresca donde ya se refresca
|
|
66
70
|
// todo: en `identify`, que es lo que corre cada vez que el acta cambia o se reconecta.
|
|
67
71
|
let actaVista = null
|
|
72
|
+
// ¿Está esta bóveda ATADA al proxio ahora mismo? Conectado no es identificado: el socket
|
|
73
|
+
// puede estar perfectamente abierto y el sobre rechazado, y entonces nadie la alcanza por
|
|
74
|
+
// su pubkey. Se expone para que `status` lo enseñe en vez de dejarlo a un log del arranque.
|
|
75
|
+
let identificado = false
|
|
68
76
|
const enElActa = (token) => {
|
|
69
77
|
// SIN ACTA NO SE NEGOCIA. No se sabe quién es nadie, y en la duda no se arranca a
|
|
70
78
|
// parsear red no confiable. Se sigue por el proxio, que es el escalón que siempre está.
|
|
71
79
|
if (!actaVista) return false
|
|
72
80
|
return (actaVista.members || []).some((m) => m?.pub === token)
|
|
73
81
|
}
|
|
74
|
-
const client = new WebSocketProxyClient({
|
|
82
|
+
const client = makeClient ? makeClient({ url, acceptDirectFrom: enElActa }) : new WebSocketProxyClient({
|
|
75
83
|
url, enableWebRTC: true, acceptDirectFrom: enElActa, autoReconnect: true,
|
|
76
84
|
maxReconnectAttempts: 100000, reconnectDelay: 4000
|
|
77
85
|
})
|
|
@@ -79,6 +87,7 @@ export async function createTransport ({ identity, dir, url = DEFAULT_PROXY, com
|
|
|
79
87
|
await client.connect()
|
|
80
88
|
|
|
81
89
|
const identify = async () => {
|
|
90
|
+
identificado = false
|
|
82
91
|
if (!client.token) return
|
|
83
92
|
const record = (await identity.profileActa?.().catch(() => null))?.acta || null
|
|
84
93
|
actaVista = record // la copia que mira la puerta del canal directo
|
|
@@ -90,6 +99,7 @@ export async function createTransport ({ identity, dir, url = DEFAULT_PROXY, com
|
|
|
90
99
|
// El sobre lo arma el pilar (`identifyAs`), que le pone el destinatario.
|
|
91
100
|
try {
|
|
92
101
|
await client.identifyAs({ publickey: comm, sign: (d) => commKey.sign(d), acta: record })
|
|
102
|
+
identificado = true
|
|
93
103
|
// SE DICE QUE SE IDENTIFICÓ, y con qué llave. Este camino era MUDO: al pasar de la
|
|
94
104
|
// maestra a la llave de comunicación, el único rastro de «estoy alcanzable» que
|
|
95
105
|
// quedaba en el log era el del repliegue. Así que cuando la consola decía «no
|
|
@@ -124,13 +134,51 @@ export async function createTransport ({ identity, dir, url = DEFAULT_PROXY, com
|
|
|
124
134
|
if (!publickey) return
|
|
125
135
|
log('[vault] identifying with the master key: this vault is not in its own record yet (open the profile once to fix it)')
|
|
126
136
|
await client.identifyAs({ publickey, sign: (d) => identity.signData(d), acta: record })
|
|
137
|
+
identificado = true
|
|
127
138
|
} catch (e) {
|
|
128
139
|
log(`[vault] cannot identify on the proxy yet: ${e.message} — the profile still opens; unlock it once and it gets its own communication key`)
|
|
129
140
|
}
|
|
130
141
|
}
|
|
131
|
-
|
|
142
|
+
/**
|
|
143
|
+
* IDENTIFICARSE PUEDE FALLAR, Y ESO NO PUEDE TUMBAR EL PERFIL — NI QUEDARSE ASÍ.
|
|
144
|
+
*
|
|
145
|
+
* Esto era `await identify()` a secas, así que cualquier «no» del proxio subía por
|
|
146
|
+
* `createTransport` hasta `startVault` y el perfil **no se abría en absoluto**: no es que
|
|
147
|
+
* se quedara sin voz, es que cada petición contestaba `profile is not open` y en el
|
|
148
|
+
* `status` el perfil salía sin huella. Y no se reintentaba nunca: `identify` solo vuelve a
|
|
149
|
+
* correr con el evento `token`, o sea al RECONECTAR, y aquí el socket estaba perfectamente
|
|
150
|
+
* conectado — lo que el proxio rechazó fue el sobre.
|
|
151
|
+
*
|
|
152
|
+
* El caso real (2026-09-10): esta máquina arranca con el reloj 35 minutos atrasado (el
|
|
153
|
+
* `systemd-timesyncd` no contactó el servidor de hora hasta media hora después del
|
|
154
|
+
* arranque), el proxio rechaza el `identify` con «ts fuera de la ventana ±5min» y la
|
|
155
|
+
* bóveda se quedaba fuera de la red DESDE EL ARRANQUE HASTA QUE ALGUIEN LA REINICIABA.
|
|
156
|
+
* Con ella fuera no hay a quién timbrar: el teléfono nunca recibía un pedido de
|
|
157
|
+
* aprobación, y desde fuera eso se ve igual que «no hay pedidos».
|
|
158
|
+
*
|
|
159
|
+
* Reintentar con espera creciente lo arregla solo en cuanto el reloj se corrige, y cada
|
|
160
|
+
* intento se dice en voz alta: una bóveda inalcanzable en silencio es el fallo que ya
|
|
161
|
+
* costó un día entero (`CLAUDE.md`, el apagón del 1-2 de septiembre).
|
|
162
|
+
*/
|
|
163
|
+
let reintento = null
|
|
164
|
+
let fallos = 0
|
|
165
|
+
const identifyWithRetry = async () => {
|
|
166
|
+
if (reintento) { clearTimeout(reintento); reintento = null }
|
|
167
|
+
try {
|
|
168
|
+
await identify()
|
|
169
|
+
if (fallos) log(`[vault] identified on the proxy after ${fallos + 1} attempt(s)`)
|
|
170
|
+
fallos = 0
|
|
171
|
+
} catch (e) {
|
|
172
|
+
fallos++
|
|
173
|
+
const espera = Math.min(60000, 5000 * fallos)
|
|
174
|
+
log(`[vault] could not identify on the proxy (${e.message}) — retrying in ${Math.round(espera / 1000)}s; until then this vault is unreachable and cannot ring anyone`)
|
|
175
|
+
reintento = setTimeout(() => { reintento = null; identifyWithRetry() }, espera)
|
|
176
|
+
reintento.unref?.()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
await identifyWithRetry()
|
|
132
180
|
// Re-identificar al reconectar (el token cambia).
|
|
133
|
-
client.on('token', () => {
|
|
181
|
+
client.on('token', () => { fallos = 0; identifyWithRetry() })
|
|
134
182
|
|
|
135
|
-
return { client, token: client.token, identify }
|
|
183
|
+
return { client, token: client.token, identify: identifyWithRetry, isIdentified: () => identificado }
|
|
136
184
|
}
|
package/src/vault.js
CHANGED
|
@@ -306,7 +306,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
306
306
|
}
|
|
307
307
|
await ensureCommKeyInActa()
|
|
308
308
|
|
|
309
|
-
const { client, identify: reidentificar } = await createTransport({ identity, dir, url: proxyUrl, commKey, log })
|
|
309
|
+
const { client, identify: reidentificar, isIdentified } = await createTransport({ identity, dir, url: proxyUrl, commKey, log })
|
|
310
310
|
|
|
311
311
|
// El registro público de cadenas de selladores: deposita, si hay a dónde, los eslabones
|
|
312
312
|
// que le dicen a un tercero si esta cuenta sigue sellada por quien él cree. Ver
|
|
@@ -3179,6 +3179,9 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
3179
3179
|
|
|
3180
3180
|
return {
|
|
3181
3181
|
identity, client, store, threads, secrets, master, fingerprint: fp, dir,
|
|
3182
|
+
// Alcanzable ≠ corriendo: si el proxio rechazó el `identify` esta bóveda está viva y
|
|
3183
|
+
// nadie puede hablarle. Lo mira `status` (ver `manager.js`, `summary`).
|
|
3184
|
+
isIdentified,
|
|
3182
3185
|
// Se expone para poder PROBAR que una respuesta que no cabe no mata la conexión: es el
|
|
3183
3186
|
// único punto por el que sale todo, y el fallo que cierra vive justo ahí.
|
|
3184
3187
|
reply,
|