@dotrino/vaultd 0.112.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vaultd",
3
- "version": "0.112.1",
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
- return `${B}${p.name || '(sin nombre)'}${Z} ${p.id} ${p.fingerprint || '—'} ${lock}`
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
- return { ...p, fingerprint: v?.fingerprint || null, iss: v?.master || null }
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
- const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
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
- await identify()
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', () => { identify().catch(() => {}) })
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,