@dotrino/vaultd 0.77.0 → 0.78.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/lib/src/admin.js CHANGED
@@ -60,7 +60,20 @@ export const ADMIN_OPS = Object.freeze([
60
60
  ])
61
61
 
62
62
  /**
63
- * LO ÚNICO QUE SE ATIENDE CON EL CANDADO ECHADO: mirar. Ni una operación que cambie algo.
63
+ * QUÉ SE ATIENDE CON EL CANDADO ECHADO.
64
+ *
65
+ * No es «solo mirar», y confundirlo fue un error: **guardar una variable NO necesita la
66
+ * maestra**. Sellar un valor usa las llaves PÚBLICAS de quien va a leerlo, así que la
67
+ * bóveda puede hacerlo sin ningún secreto suyo (`docs/secretos-sellados.md` §8.1) — es
68
+ * justo lo que hace barata la rotación. Meterlo en el mismo saco que revocar dejaba la
69
+ * consola sin poder configurar nada con el perfil cerrado, que es su trabajo diario.
70
+ *
71
+ * Lo que SÍ se queda fuera es `revoke`: reescribe el acta, y sellar el acta es de la
72
+ * maestra. Cerrada no hay con qué.
73
+ *
74
+ * (Si el cajón tiene su llave sellada con la frase, repartirla a los miembros puede quedar
75
+ * a deber hasta que alguien abra: eso ya lo dice la bóveda y se salda solo. Guardar el
76
+ * valor, que es lo que se pidió, se hace igual.)
64
77
  *
65
78
  * El candado ES de la consola —los aparatos ya emparejados siguen leyendo, guardando y
66
79
  * firmando con SU llave— así que cerrar el perfil tiene que cerrar exactamente esto. No
@@ -72,7 +85,7 @@ export const ADMIN_OPS = Object.freeze([
72
85
  * Se dejan las de LEER a propósito: si se cortaran todas, la consola no podría ni
73
86
  * enterarse de por qué no funciona y parecería rota. Así puede decir «está cerrada».
74
87
  */
75
- export const ADMIN_OPS_WHILE_LOCKED = Object.freeze(['audit', 'vars'])
88
+ export const ADMIN_OPS_WHILE_LOCKED = Object.freeze(['audit', 'vars', 'var.set', 'var.setMany'])
76
89
 
77
90
  /** Cuánto se recuerda un nonce ya usado (el doble de la ventana de frescura). */
78
91
  export const ADMIN_NONCE_TTL_MS = 10 * 60 * 1000
@@ -136,7 +149,7 @@ export function createAdminDesk ({
136
149
  }
137
150
  const by = await deviceIdOf(chk.device).catch(() => null)
138
151
 
139
- // CERRADA NO SE ADMINISTRA. Va después de autorizar (el candado es estado del perfil,
152
+ // CERRADA NO SE SELLA EL ACTA. Va después de autorizar (el candado es estado del perfil,
140
153
  // y a un desconocido no se le cuenta) y ANTES de quemar el nonce: quien reintente
141
154
  // tras abrir la bóveda no tiene por qué generar otro.
142
155
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vaultd",
3
- "version": "0.77.0",
3
+ "version": "0.78.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/transport.js CHANGED
@@ -66,7 +66,16 @@ export async function createTransport ({ identity, dir, url = DEFAULT_PROXY, com
66
66
  if (comm && esMiembro) {
67
67
  const data = { op: 'identify', publickey: comm, token: client.token, ts: Date.now() }
68
68
  const signature = await commKey.sign(data)
69
- if (signature) return client.identify({ data, signature, acta: record })
69
+ if (signature) {
70
+ await client.identify({ data, signature, acta: record })
71
+ // SE DICE QUE SE IDENTIFICÓ, y con qué llave. Este camino era MUDO: al pasar de la
72
+ // maestra a la llave de comunicación, el único rastro de «estoy alcanzable» que
73
+ // quedaba en el log era el del repliegue. Así que cuando la consola decía «no
74
+ // contesta», no había forma de saber desde aquí si la bóveda estaba atada al proxio
75
+ // o no. Una línea por reconexión es barata; no tenerla cuesta una hora de sondeos.
76
+ log('[vault] identified on the proxy with the communication key · record #%s', record?.seq ?? '?')
77
+ return
78
+ }
70
79
  }
71
80
 
72
81
  // REPLIEGUE, y solo para migrar: una bóveda que todavía no ha metido su llave de
package/src/vault.js CHANGED
@@ -285,8 +285,26 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
285
285
  return new Set(revoked.map((r) => r.nonce))
286
286
  }
287
287
 
288
+ /**
289
+ * CONTESTAR SIN MATAR LA CONEXIÓN.
290
+ *
291
+ * El proxio corta los frames a 1 MB (`maxPayload`), y `ws` no «descarta» el que se pasa:
292
+ * CIERRA EL SOCKET con un 1009. La bóveda se queda muda, sin un solo error en su log, y
293
+ * desde fuera se ve igual que si estuviera apagada. Eso ya pasó y duró tres días.
294
+ *
295
+ * Así que una respuesta que no quepa no se manda: se sustituye por un error, que sí cabe.
296
+ * Una bóveda que dice «no cabe» se arregla; una muda no se puede ni diagnosticar.
297
+ */
288
298
  const reply = (to, obj) => {
289
- try { client.send(to, obj) } catch (e) { log('[vault] could not reply:', e.message) }
299
+ try {
300
+ const bytes = Buffer.byteLength(JSON.stringify(obj))
301
+ if (bytes > MAX_REPLY_BYTES) {
302
+ log(`[vault] the reply to ${obj?.type} does not fit (${bytes} bytes > ${MAX_REPLY_BYTES}): sending an error instead of killing the connection`)
303
+ audit('reply-too-big', { type: obj?.type || null, bytes })
304
+ return client.send(to, { type: MSG.ERROR, error: `reply too big (${bytes} bytes): ask for less at a time` })
305
+ }
306
+ client.send(to, obj)
307
+ } catch (e) { log('[vault] could not reply:', e.message) }
290
308
  }
291
309
 
292
310
  // FRESCURA anti-replay: toda petición firmada debe traer `data.ts` dentro de una
@@ -680,7 +698,9 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
680
698
  if (!msg.chain?.length) msg.chain = null
681
699
  const left = msg.chain?.length || 0
682
700
  log(`[vault] record chain trimmed to fit the transport: ${left}/${full} link(s), ${size(msg)} bytes`)
683
- if (size(msg) > MAX_REPLY_BYTES) log(`[vault] ⚠ the reply STILL does not fit (${size(msg)} bytes): the proxy will drop it`)
701
+ // Si tras podar la cadena SIGUE sin caber, no se avisa y se manda igual —eso mataba la
702
+ // conexión—: se devuelve tal cual y `reply` lo sustituye por un error. El aviso de aquí
703
+ // sobra porque el de `reply` dice lo mismo y además dice qué hizo.
684
704
  return msg
685
705
  }
686
706
 
@@ -2031,6 +2051,9 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
2031
2051
 
2032
2052
  return {
2033
2053
  identity, client, store, threads, secrets, master, fingerprint: fp, dir,
2054
+ // Se expone para poder PROBAR que una respuesta que no cabe no mata la conexión: es el
2055
+ // único punto por el que sale todo, y el fallo que cierra vive justo ahí.
2056
+ reply,
2034
2057
  /**
2035
2058
  * SOLTAR LA MAESTRA. Cerrar el perfil tiene que sacarla de la memoria, no solo dejar de
2036
2059
  * cargarla en el siguiente arranque: si no, el candado seguiría siendo una bandera al