@dotrino/vaultd 0.111.1 → 0.112.1

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 CHANGED
@@ -846,6 +846,7 @@ ed25519 en formato OpenSSH (sin frase: la bóveda es el candado) y RSA/P-256 en
846
846
  - [`acta-de-perfil.md`](./docs/acta-de-perfil.md) — el modelo vigente: un perfil es un conjunto de llaves con un acta firmada por un solo sellador.
847
847
  - [`pairing-protocol.md`](./docs/pairing-protocol.md) — el emparejamiento endurecido: por qué el token dejó de ser autoridad suficiente.
848
848
  - [`vinculacion-de-cuentas.md`](./docs/vinculacion-de-cuentas.md) — los dos caminos al conectar un aparato, y por qué no existe fusionar cuentas.
849
+ - [`inicio-de-sesion.md`](./docs/inicio-de-sesion.md) — **entrar sin enrolar**: las sesiones (papel con vencimiento, respaldado por un aparato tuyo) y el orden hacia el inicio de sesión federado.
849
850
  - [`store-identity-architecture.md`](./docs/store-identity-architecture.md) — por qué la bóveda del PC hace de store además de identidad.
850
851
 
851
852
  Sin anuncios, sin cuentas, sin rastreo. MIT · parte de Dotrino.
package/lib/src/index.js CHANGED
@@ -97,9 +97,8 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
97
97
  const selfCert = await getSelfCert()
98
98
  const identify = async () => {
99
99
  if (!client.token) return
100
- const data = { op: 'identify', publickey: iss, token: client.token, ts: Date.now() }
101
- const { signature } = await identity.signData(data)
102
- await client.identify({ data, signature, cert: selfCert })
100
+ // El sobre lo arma el pilar (`identifyAs`), que le pone el destinatario.
101
+ await client.identifyAs({ publickey: iss, sign: (d) => identity.signData(d), cert: selfCert })
103
102
  }
104
103
  await identify()
105
104
  client.on('token', () => identify().catch(() => {}))
@@ -85,7 +85,15 @@ export async function connectLocal ({ masterId, timeoutMs = 3000 } = {}) {
85
85
  // El mostrador local no pide identificarse: la puerta la abre el certificado que va
86
86
  // dentro de cada mensaje, igual que por el proxio. Identificarse ahí sirve para ser
87
87
  // DIRECCIONABLE, y aquí no hay a quién direccionar.
88
+ //
89
+ // LAS DOS FORMAS, porque quien llama no sabe por dónde salió. `freshClient` devuelve
90
+ // esto o un `WebSocketProxyClient` según haya socket o no, y el de arriba llama al
91
+ // método sin más: si aquí falta el que el pilar usa ahora, el escalón 1 del transporte
92
+ // —el más rápido, y el que corre en la máquina del dueño— muere con «client.identifyAs
93
+ // is not a function» mientras por el proxio todo va bien. Que sea un no-op no lo hace
94
+ // opcional: es la forma del cliente, y tiene que estar entera.
88
95
  identify: async () => ({ ok: true }),
96
+ identifyAs: async () => ({ ok: true }),
89
97
  on: (ev, fn) => {
90
98
  if (ev !== 'message') return () => {}
91
99
  oyentes.add(fn)
@@ -199,9 +199,8 @@ async function freshClient (proxyUrl, connectTimeoutMs = 20000, master = null) {
199
199
 
200
200
  /** Identifica la conexión bajo la pubkey del servicio (para ser direccionable). */
201
201
  async function identifyAsService (client, device) {
202
- const data = { op: 'identify', publickey: device.publickey, token: client.token, ts: Date.now() }
203
- const { signature } = await signWithDevice({ privateJwk: device.privateJwk, data })
204
- await client.identify({ data, signature })
202
+ // El sobre lo arma el pilar (`identifyAs`), que le pone el destinatario.
203
+ await client.identifyAs({ publickey: device.publickey, sign: (d) => signWithDevice({ privateJwk: device.privateJwk, data: d }) })
205
204
  }
206
205
 
207
206
  /** Cuánto espera un agente a que alguien apruebe (el pedido vive 5 min en la bóveda). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vaultd",
3
- "version": "0.111.1",
3
+ "version": "0.112.1",
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": {
@@ -24,7 +24,7 @@
24
24
  "@dotrino/compat": "0.4.0",
25
25
  "@dotrino/identity": "0.81.0",
26
26
  "@dotrino/passmanager": "^0.10.1",
27
- "@dotrino/proxy-client": "0.15.0",
27
+ "@dotrino/proxy-client": "0.18.2",
28
28
  "@dotrino/roadmap": "0.2.0",
29
29
  "ws": "^8.18.0"
30
30
  },
package/src/client.js CHANGED
@@ -40,9 +40,8 @@ async function freshClient ({ proxyUrl, dir = '.dotrino-vault-device' }) {
40
40
  /** Identifica la conexión bajo la pubkey de dispositivo D (para ser direccionable). */
41
41
  async function identifyAsDevice (client, device) {
42
42
  if (!client.token) return
43
- const data = { op: 'identify', publickey: device.publickey, token: client.token, ts: Date.now() }
44
- const { signature } = await signWithDevice({ privateJwk: device.privateJwk, data })
45
- await client.identify({ data, signature })
43
+ // El sobre lo arma el pilar (`identifyAs`), que le pone el destinatario.
44
+ await client.identifyAs({ publickey: device.publickey, sign: (d) => signWithDevice({ privateJwk: device.privateJwk, data: d }) })
46
45
  }
47
46
 
48
47
  function waitFor (client, predicate, timeoutMs = 30000) {
package/src/replica.js CHANGED
@@ -132,8 +132,8 @@ export async function runReplica ({ dir = replicaDir(), proxyUrl, log = console.
132
132
 
133
133
  const client = new WebSocketProxyClient({ url })
134
134
  await client.connect()
135
- const data = { op: 'identify', publickey: link.device.publickey, token: client.token, ts: Date.now() }
136
- await client.identify({ data, signature: (await firmar(data)).signature })
135
+ // El sobre lo arma el pilar (`identifyAs`), que le pone el destinatario.
136
+ await client.identifyAs({ publickey: link.device.publickey, sign: (d) => firmar(d) })
137
137
  log(`[replica] ${id} · connected to ${url}`)
138
138
  log(`[replica] record #${store.seq}${store.acta ? '' : ' · no record yet: waiting for the vault'}`)
139
139
 
package/src/transport.js CHANGED
@@ -87,10 +87,9 @@ export async function createTransport ({ identity, dir, url = DEFAULT_PROXY, com
87
87
  const esMiembro = comm && (record?.members || []).some((m) => m?.pub === comm)
88
88
 
89
89
  if (comm && esMiembro) {
90
- const data = { op: 'identify', publickey: comm, token: client.token, ts: Date.now() }
91
- const signature = await commKey.sign(data)
92
- if (signature) {
93
- await client.identify({ data, signature, acta: record })
90
+ // El sobre lo arma el pilar (`identifyAs`), que le pone el destinatario.
91
+ try {
92
+ await client.identifyAs({ publickey: comm, sign: (d) => commKey.sign(d), acta: record })
94
93
  // SE DICE QUE SE IDENTIFICÓ, y con qué llave. Este camino era MUDO: al pasar de la
95
94
  // maestra a la llave de comunicación, el único rastro de «estoy alcanzable» que
96
95
  // quedaba en el log era el del repliegue. Así que cuando la consola decía «no
@@ -98,6 +97,11 @@ export async function createTransport ({ identity, dir, url = DEFAULT_PROXY, com
98
97
  // o no. Una línea por reconexión es barata; no tenerla cuesta una hora de sondeos.
99
98
  log(`[vault] identified on the proxy with the communication key · record #${record?.seq ?? '?'}`)
100
99
  return
100
+ } catch (e) {
101
+ // SOLO «esta llave no firma» cae al camino de abajo, y se distingue por el `code`.
102
+ // Un fallo de red no es eso: tragárselo aquí mandaría a la maestra a firmar por un
103
+ // problema pasajero, que es justo lo que la llave de comunicación vino a evitar.
104
+ if (e?.code !== 'no-signature') throw e
101
105
  }
102
106
  }
103
107
 
@@ -118,10 +122,8 @@ export async function createTransport ({ identity, dir, url = DEFAULT_PROXY, com
118
122
  try {
119
123
  const publickey = await masterPubkeyOf(identity)
120
124
  if (!publickey) return
121
- const data = { op: 'identify', publickey, token: client.token, ts: Date.now() }
122
- const { signature } = await identity.signData(data)
123
125
  log('[vault] identifying with the master key: this vault is not in its own record yet (open the profile once to fix it)')
124
- await client.identify({ data, signature, acta: record })
126
+ await client.identifyAs({ publickey, sign: (d) => identity.signData(d), acta: record })
125
127
  } catch (e) {
126
128
  log(`[vault] cannot identify on the proxy yet: ${e.message} — the profile still opens; unlock it once and it gets its own communication key`)
127
129
  }
package/src/tui/app.js CHANGED
@@ -1333,7 +1333,9 @@ async function onKeySecrets (term, st, key) {
1333
1333
  } else if (ch === 't' && cur?.key) {
1334
1334
  await toggleVisibility(term, st, cur.public, () => vc.setSecretVisibility(cur.ns, cur.key, !cur.public, activeId(st), activePwd(st)))
1335
1335
  } else if (ch === 'r' && cur?.key) {
1336
- await promptRenameVariable(term, st, cur)
1336
+ // SIN `await`: el prompt solo ABRE el cuadro de entrada y vuelve. Esperarlo aquí
1337
+ // congelaba la TUI entera — ver `promptRenameVariable`.
1338
+ promptRenameVariable(term, st, cur)
1337
1339
  } else if (key.name === 'f5') {
1338
1340
  await refreshSecrets(term, st)
1339
1341
  }
@@ -1349,24 +1351,33 @@ async function onKeySecrets (term, st, key) {
1349
1351
  * cambio de configuración, así que se va a reiniciar. Si su código busca el nombre viejo,
1350
1352
  * dejará de encontrarlo — y eso no lo puede saber la bóveda.
1351
1353
  */
1354
+ /**
1355
+ * ABRE EL CUADRO Y VUELVE. No devuelve una promesa que espere a que el usuario escriba, y
1356
+ * eso no es estilo: era un INTERBLOQUEO que congelaba la TUI entera.
1357
+ *
1358
+ * Quien escribe es `onInputKey`, y a `onInputKey` solo se llega desde el bucle de teclas.
1359
+ * Devolver aquí una promesa que solo resuelve en `onSubmit` dejaba al bucle esperándola —
1360
+ * o sea, esperando a una tecla que no iba a leer nunca, porque el que lee es él. A partir
1361
+ * de ahí no respondía a nada: ni Enter, ni Esc, ni Ctrl-C, que también se atienden ahí. La
1362
+ * única salida era cerrar la terminal, y es lo que le tocó hacer al dueño el 2026-09-05.
1363
+ *
1364
+ * Los otros veinte prompts de esta TUI ya eran así; este era el único que no.
1365
+ */
1352
1366
  function promptRenameVariable (term, st, cur) {
1353
1367
  const i = L(st)
1354
- return new Promise((listo) => {
1355
- setInput(st, {
1356
- label: i.renameLabel(cur.key),
1357
- hint: i.renameHint,
1358
- value: cur.key,
1359
- onSubmit: async (nuevo) => {
1360
- const nv = nuevo.trim()
1361
- st.input = null
1362
- if (!nv || nv === cur.key) { listo(); return }
1363
- if (!KEY_RE.test(nv)) { flash(st, i.keyInvalid, 'danger'); listo(); return }
1364
- const r = await guard(term, st, i.renaming, () => vc.renameSecret(cur.ns, cur.key, nv, activeId(st)))
1365
- if (r.ok) { flash(st, i.renamed(cur.key, nv)); st.secrets = r.v }
1366
- listo()
1367
- },
1368
- onCancel: () => { st.input = null; listo() }
1369
- })
1368
+ setInput(st, {
1369
+ label: i.renameLabel(cur.key),
1370
+ hint: i.renameHint,
1371
+ value: cur.key,
1372
+ onSubmit: async (nuevo) => {
1373
+ const nv = nuevo.trim()
1374
+ st.input = null
1375
+ if (!nv || nv === cur.key) return
1376
+ if (!KEY_RE.test(nv)) { flash(st, i.keyInvalid, 'danger'); return }
1377
+ const r = await guard(term, st, i.renaming, () => vc.renameSecret(cur.ns, cur.key, nv, activeId(st)))
1378
+ if (r.ok) { flash(st, i.renamed(cur.key, nv)); st.secrets = r.v }
1379
+ },
1380
+ onCancel: () => { st.input = null }
1370
1381
  })
1371
1382
  }
1372
1383
 
@@ -1971,4 +1982,4 @@ export async function runTui () {
1971
1982
  }
1972
1983
 
1973
1984
  // Solo para pruebas headless (render sin terminal real). No usar en runtime.
1974
- export const __test = { render, activeLocked, autoLockedIds, autoLockWakeIn, forgetAutoLocked, autoLockMin, refreshAll, ensureUnlocked, profileRows, deviceRows, secretRows, devVarRows, meRows, capsRows, onKeyCaps, pairModeRows, pairingBody, scrollBody, fitHelp, toggleLang, mergeMembersAndCerts, seguirAqui, resetToque: () => { ultimoToque = 0 } }
1985
+ export const __test = { render, onKeySecrets, activeLocked, autoLockedIds, autoLockWakeIn, forgetAutoLocked, autoLockMin, refreshAll, ensureUnlocked, profileRows, deviceRows, secretRows, devVarRows, meRows, capsRows, onKeyCaps, pairModeRows, pairingBody, scrollBody, fitHelp, toggleLang, mergeMembersAndCerts, seguirAqui, resetToque: () => { ultimoToque = 0 } }
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 } = await createTransport({ identity, dir, url: proxyUrl, commKey, log })
309
+ const { client, identify: reidentificar } = 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
@@ -1263,6 +1263,27 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
1263
1263
  if (!publicOnly && needsApproval(chk.device, record)) {
1264
1264
  const deviceId = await deviceIdOf(chk.device).catch(() => null)
1265
1265
  const label = (record?.members || []).find((m) => m.pub === chk.device)?.label || ''
1266
+ // NADIE PUEDE APROBAR = SE DICE AHORA, no se deja esperando cinco minutos.
1267
+ //
1268
+ // La bóveda ya lo sabía —lo escribía en su propio log, «rang 0 approver(s)»— y aun
1269
+ // así contestaba «pendiente» y dejaba al otro aguardando a algo que no podía pasar.
1270
+ // Eso es un fallo ruidoso convertido en uno mudo, que es exactamente lo que la norma
1271
+ // prohíbe (CLAUDE.md, «nada de repliegues»), y encima el que sabe es el ÚNICO que
1272
+ // puede enterar al otro (CONVENCIONES §14): el que pide no tiene el acta, así que no
1273
+ // hay forma de que lo averigüe por su cuenta.
1274
+ //
1275
+ // Lo pidió el dueño el 2026-09-05, después de mirar un `dotrino-env check` que parecía
1276
+ // colgado y era esto.
1277
+ const quienAprueba = (record?.members || []).filter((m) => Acta.memberCan(record, m.pub, 'approve'))
1278
+ if (!quienAprueba.length) {
1279
+ audit('rejected', { what: 'secrets', ns, reason: 'sin-aprobador' })
1280
+ log(`[vault] ${ns}: refused ${deviceId || '????-????'} — nobody in the record can approve`)
1281
+ return reply(from, {
1282
+ type: MSG.ERROR,
1283
+ code: 'no-approver',
1284
+ error: `approval: nobody in the record can approve — grant it with \`dotrino-vault caps <ID> +aprueba\`, or let this service run unattended with \`dotrino-vault caps ${deviceId || '<ID>'} +desatendido\``
1285
+ })
1286
+ }
1266
1287
  // `from` SE GUARDA: la respuesta llega más tarde —cuando alguien apruebe— y tiene que
1267
1288
  // volver POR DONDE VINO. Si la pregunta entró por el mostrador local, mandarla por el
1268
1289
  // proxio la deja en el vacío: quien preguntó está escuchando en el socket, no ahí.
@@ -3181,7 +3202,14 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
3181
3202
  // ABRIR ES EL ÚNICO MOMENTO en que un perfil con contraseña está abierto, así que es
3182
3203
  // aquí donde puede estrenar su llave de comunicación (ver `ensureCommKeyInActa`).
3183
3204
  // Si falla, no se arrastra: abrir el perfil no puede depender de esto.
3184
- if (r?.locked === false) { try { await ensureCommKeyInActa() } catch (_) {} }
3205
+ //
3206
+ // Y SE VUELVE A IDENTIFICAR EN EL ACTO: `identify` solo corre al conectar y al cambiar
3207
+ // el token, así que estrenar la llave no servía de nada hasta el siguiente arranque —
3208
+ // la bóveda seguía muda en el proxio con su llave ya admitida en el acta. Al dueño le
3209
+ // pasó el 2026-09-05: hizo lo que el log pedía y la consola siguió sin contestar.
3210
+ if (r?.locked === false) {
3211
+ try { if (await ensureCommKeyInActa()) await reidentificar?.() } catch (_) {}
3212
+ }
3185
3213
  // Y ABSORBER LO PENDIENTE. La subacta ya lo estaba honrando con el perfil cerrado —de
3186
3214
  // eso vive—, pero mientras siga ahí solo lo sabe esta máquina: al sellarlo, entra en
3187
3215
  // el acta y lo respeta toda la cuenta. Si falla no se arrastra: abrir el perfil no