@dotrino/vaultd 0.76.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.76.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/ctl.js CHANGED
@@ -419,7 +419,14 @@ async function cmdJoin (rest) {
419
419
  process.exit(2)
420
420
  }
421
421
  const qr = parseInvite(texto)
422
- if (!qr?.sn || !qr?.iss || !qr?.proxy) {
422
+ // QUÉ HACE FALTA DE VERDAD: el nonce de la sesión (`sn`) y una forma de alcanzar a la
423
+ // otra bóveda — la CITA del proxio (`conn`) en la invitación corta, o su llave (`iss`)
424
+ // en la larga. Quien la consume es `enrollDevice`, que sabe canjear la cita.
425
+ //
426
+ // Esto exigía `iss` SIEMPRE, y la invitación corta no lo lleva (la llave dejó de viajar
427
+ // cuando el QR se acortó, `lib/src/invite.js`). O sea que `join` rechazaba todas las
428
+ // invitaciones que emite `pair` hoy: el multivault entero no se podía montar.
429
+ if (!qr?.sn || !(qr?.conn || qr?.iss)) {
423
430
  console.error('Esa invitación no se entiende. Pega la línea completa que imprime «dotrino-vault pair».')
424
431
  process.exit(2)
425
432
  }
package/src/daemon.js CHANGED
@@ -791,7 +791,10 @@ export async function runDaemon () {
791
791
 
792
792
  let qr = null
793
793
  try { qr = parseInvite(texto) } catch (_) {}
794
- if (!qr?.sn || !qr?.iss || !qr?.proxy) {
794
+ // Lo mismo que `dotrino-vault join` (ver `ctl.js`): basta `sn` + la cita del proxio o
795
+ // la llave. Exigir `iss` dejaba fuera a la invitación corta, que es la única que emite
796
+ // `pair` hoy — así que este camino de despliegue no había forma de que funcionara.
797
+ if (!qr?.sn || !(qr?.conn || qr?.iss)) {
795
798
  console.error('[vault] the invitation in DOTRINO_JOIN is not valid; ignoring it')
796
799
  return
797
800
  }
@@ -799,7 +802,7 @@ export async function runDaemon () {
799
802
  const hecho = readJsonSafe(marca)
800
803
  if (hecho?.sn === qr.sn) return // ya se usó: un reinicio no vuelve a pedir entrar
801
804
 
802
- writeJson(marca, { at: Date.now(), sn: qr.sn, iss: qr.iss })
805
+ writeJson(marca, { at: Date.now(), sn: qr.sn, ...(qr.iss ? { iss: qr.iss } : {}) })
803
806
  writeJson(path.join(dir, 'join-request.json'), {
804
807
  qr,
805
808
  label: 'bóveda',
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