@dotrino/vaultd 0.58.0 → 0.60.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/enroll.js CHANGED
@@ -277,7 +277,12 @@ export function createEnrollDesk ({
277
277
  pend.continuity = (okC && d.continuity.member === d.dpub) ? d.continuity : null
278
278
  }
279
279
  pend.from = from // la bóveda NO conoce el código: lo aprende cuando lo tipeas
280
- if (d.label) pend.label = String(d.label).slice(0, 60)
280
+ // EL NOMBRE QUE PUSISTE AQUÍ MANDA. El aparato manda el suyo al enrolarse, y hasta
281
+ // ahora pisaba siempre al de la bóveda — como el aparato usa por defecto el apodo del
282
+ // PERFIL, acababas con varios dispositivos llamados igual que tú y sin forma de saber
283
+ // cuál era cuál. Si en `pair` le diste un nombre, ese es el nombre; el del aparato
284
+ // sigue valiendo como propuesta cuando no dijiste nada.
285
+ if (d.label && !pend.label) pend.label = String(d.label).slice(0, 60)
281
286
  // Camino A: de qué cuenta estamos hablando. Se guarda para poder comprobar, cuando
282
287
  // llegue el acta sellada, que es la que este dispositivo dijo que iba a entregar.
283
288
  if (intent === 'adopt' && typeof d.profileId === 'string') pend.profileId = d.profileId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vaultd",
3
- "version": "0.58.0",
3
+ "version": "0.60.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
@@ -205,6 +205,17 @@ async function cmdPair (args = []) {
205
205
  // --approval: el aparato que entre pedirá tu aprobación (teléfono) en cada petición de
206
206
  // claves privadas. Por defecto NO pide; se cambia después con `caps <ID> +permiso`.
207
207
  const approval = args.includes('--approval')
208
+ // --name <n>: CÓMO SE VA A LLAMAR el aparato que entre, decidido aquí y antes de nada.
209
+ // Sin esto el nombre lo ponía el propio aparato, que por defecto usa el apodo del
210
+ // PERFIL: acababas con varios dispositivos llamados igual que tú, y para distinguirlos
211
+ // había que renombrarlos después —cuando ya no sabías cuál era cuál—.
212
+ const nIdx = args.indexOf('--name')
213
+ let label = null
214
+ if (nIdx >= 0) {
215
+ const v = args[nIdx + 1]
216
+ if (!v || v.startsWith('-')) { console.error('uso: dotrino-vault pair --name "teléfono de casa"'); process.exit(2) }
217
+ label = v.slice(0, 60)
218
+ }
208
219
  // --admin: el aparato que entre por esta invitación podrá ADMINISTRAR (la consola).
209
220
  // El QR no lleva nada: es una nota local de la bóveda y el permiso se aplica al aprobar
210
221
  // con el código que tecleas aquí. Ahorra el `caps <ID> +administra` de después, que en un
@@ -258,9 +269,11 @@ async function cmdPair (args = []) {
258
269
  // meter el dispositivo en una cuenta que ya vive aquí, se ESTRENA una (vacía) y
259
270
  // entra a ella; las demás no se tocan. En la TUI esto es una pregunta con sus
260
271
  // opciones; en la CLI es una bandera, para que siga sirviendo en un script.
272
+ // `--name` lleva su valor detrás: no puede confundirse con el nombre de la cuenta.
273
+ const consumido = (i) => nIdx >= 0 && i === nIdx + 1
261
274
  const naIdx = args.findIndex((a) => a === '--new-account')
262
275
  if (naIdx >= 0) {
263
- const next = args[naIdx + 1]
276
+ const next = consumido(naIdx + 1) ? null : args[naIdx + 1]
264
277
  const name = (next && !next.startsWith('-')) ? next : `cuenta ${new Date().toISOString().slice(0, 10)}`
265
278
  const d = await profileRequest('add', { name, ...(pairKek ? { kek: pairKek } : {}) })
266
279
  if (d.error) { console.error('%s', d.error); process.exit(1) }
@@ -275,7 +288,7 @@ async function cmdPair (args = []) {
275
288
  const adIdx = args.findIndex((a) => a === '--adopt')
276
289
  const adopt = adIdx >= 0
277
290
  if (adopt) {
278
- const next = args[adIdx + 1]
291
+ const next = consumido(adIdx + 1) ? null : args[adIdx + 1]
279
292
  const name = (next && !next.startsWith('-')) ? next : `cuenta del dispositivo`
280
293
  const d = await profileRequest('add', { name, adopt: true, ...(pairKek ? { kek: pairKek } : {}) })
281
294
  if (d.error) { console.error('%s', d.error); process.exit(1) }
@@ -298,7 +311,7 @@ async function cmdPair (args = []) {
298
311
 
299
312
  // La petición se escribe SIEMPRE (aunque no haya --service): lleva a qué perfil
300
313
  // se empareja el dispositivo.
301
- writeReq('pair-request.json', { ...(service ? { service } : {}), ...(scope ? { scope } : {}), ...(adopt ? { mode: 'adopt' } : {}), ...(approval ? { approval: true } : {}), ...(admin ? { admin: true } : {}) })
314
+ writeReq('pair-request.json', { ...(service ? { service } : {}), ...(scope ? { scope } : {}), ...(adopt ? { mode: 'adopt' } : {}), ...(approval ? { approval: true } : {}), ...(admin ? { admin: true } : {}), ...(label ? { label } : {}) })
302
315
  sendSignal(s.pid, 'SIGUSR1')
303
316
 
304
317
  let pair = null
@@ -1469,6 +1482,9 @@ function help () {
1469
1482
  estrena una cuenta VACÍA en este vault y mete ahí al dispositivo
1470
1483
  (sin la bandera entra a la cuenta activa, o a la de --profile)
1471
1484
  pair --service <ns> empareja un SERVICIO (proxy, geo…) con acceso SOLO a sus secretos
1485
+ pair --name <nombre> cómo se llamará el aparato que entre. Sin esto el nombre lo pone
1486
+ ÉL, y por defecto usa el apodo del perfil: acabas con varios
1487
+ dispositivos llamados igual que tú
1472
1488
  pair --approval el aparato que entre pedirá tu aprobación (teléfono) al recibir claves
1473
1489
  pair --admin el aparato que entre podrá ADMINISTRAR (es lo que es una consola).
1474
1490
  El QR no lleva nada: el permiso se aplica al aprobar el código aquí
package/src/tui/app.js CHANGED
@@ -127,6 +127,10 @@ function mergeMembersAndCerts (members, issued) {
127
127
  label: m.label || cert?.label || '',
128
128
  isMaster: !!m.isMaster,
129
129
  cn: m.cn || null,
130
+ // CUÁNDO ENTRÓ. Venía en el acta y se perdía aquí, así que la lista no podía
131
+ // enseñarlo por mucho que se quisiera. El nombre lo pone el propio aparato y muchas
132
+ // veces no distingue nada: la fecha es lo que deja reconocer cuál es cuál.
133
+ addedAt: m.addedAt || null,
130
134
  // El master es la propia bóveda: no tiene (ni necesita) certificado. Un servicio
131
135
  // tampoco lleva uno de dispositivo. Marcarlos «sin acceso» sería una alarma falsa.
132
136
  noAccess: !cert && !m.isMaster && !m.cn
@@ -260,6 +264,7 @@ function deviceRows (st, t) {
260
264
  const label = d.label || t.muted(i.noLabel)
261
265
  const vars = devVarsOf(st, d.sub).length
262
266
  const debt = debtOf(st, d.sub)
267
+ const desde = d.addedAt ? t.muted(` ${i.deviceSince}:${fmtExp(d.addedAt)}`) : ''
263
268
  const extra = (d.certCount > 1 ? t.muted(` certs:${d.certCount}`) : '') +
264
269
  (vars ? t.muted(` vars:${vars}`) : '') +
265
270
  // EN DEUDA: en el acta y sin poder abrir lo suyo. Va en color de aviso al lado de
@@ -272,7 +277,7 @@ function deviceRows (st, t) {
272
277
  : d.isMaster
273
278
  ? t.muted(i.thisVault)
274
279
  : t.muted('scope:' + shortScope(d.scope)) + ' ' + t.muted('exp:' + fmtExp(d.exp))
275
- rows.push({ text: ` ${t.bold(d.deviceId)} ${label} ${status}${extra}`, sel: true, meta: d })
280
+ rows.push({ text: ` ${t.bold(d.deviceId)} ${label}${desde} ${status}${extra}`, sel: true, meta: d })
276
281
  }
277
282
  const revoked = st.devices?.revoked || []
278
283
  if (revoked.length) {
@@ -318,7 +323,17 @@ function pairModeRows (st, t) {
318
323
  * una marca de si los tiene. El de administrar va aparte y avisado: es el único que deja
319
324
  * a ese aparato meter y sacar dispositivos sin venir aquí.
320
325
  */
321
- const CAPS_ORDER = ['sign', 'store', 'read', 'admin', 'passwords']
326
+ /**
327
+ * TODOS los permisos que el acta reconoce para un aparato, en el orden en que se leen: de
328
+ * lo cotidiano a lo que cambia quién manda.
329
+ *
330
+ * `approve` y `sealer` faltaban, y solo se podían conceder por la CLI. Un permiso que
331
+ * existe y no se ve en la pantalla que se llama «permisos» hace creer que no existe — y
332
+ * `sealer` es justo el que hay que entender para el multivault. Se ven siempre, aunque en
333
+ * un teléfono normal `sealer` no lo vaya a usar nadie: verlo apagado explica el modelo
334
+ * (dueño, 2026-08-31).
335
+ */
336
+ const CAPS_ORDER = ['sign', 'store', 'read', 'admin', 'approve', 'passwords', 'sealer']
322
337
 
323
338
  function capsRows (st, t) {
324
339
  const i = L(st)
@@ -918,12 +933,41 @@ async function descartarCuenta (term, st, id, volverA) {
918
933
  if (r.ok) flash(st, i.accountDiscarded)
919
934
  }
920
935
 
936
+ /**
937
+ * PRIMERO EL NOMBRE, y luego el QR.
938
+ *
939
+ * Sin preguntarlo, el nombre lo pone el propio aparato al enrolarse, y por defecto usa el
940
+ * apodo del PERFIL: te quedaban tres dispositivos llamados igual que tú y ninguna forma de
941
+ * saber cuál era cuál sin renombrarlos después —cuando ya no te acordabas—. Se pregunta
942
+ * ANTES porque después ya estás mirando un QR con el teléfono en la mano.
943
+ *
944
+ * Enter en blanco sigue valiendo: deja que lo diga el aparato, que es el comportamiento de
945
+ * siempre para quien no quiera decidirlo.
946
+ */
947
+ function beginPairingNamed (term, st, profile, service = null, after = null) {
948
+ const i = L(st)
949
+ setInput(st, {
950
+ label: i.nameDeviceLabel,
951
+ hint: i.nameDeviceHint,
952
+ onSubmit: async (nombre) => {
953
+ st.input = null
954
+ const ok = await beginPairing(term, st, profile, service, (nombre || '').trim().slice(0, 60) || null)
955
+ // `after` es para quien tenga que limpiar si el emparejamiento no llega a abrirse
956
+ // (la cuenta que nació para él). Va aquí porque con el nombre de por medio ya no se
957
+ // puede mirar el valor de retorno desde fuera.
958
+ if (after) await after(ok)
959
+ },
960
+ onCancel: () => { st.input = null }
961
+ })
962
+ return true
963
+ }
964
+
921
965
  /**
922
966
  * Abre el emparejamiento contra `profile` y salta a la pantalla del QR. Con `service`,
923
967
  * el QR es el de un SERVICIO de ese namespace (cert limitado a sus variables).
924
968
  */
925
- async function beginPairing (term, st, profile, service = null) {
926
- const r = await guard(term, st, L(st).startingPairing, () => vc.startPairing({ profile, ...(service ? { service } : {}) }))
969
+ async function beginPairing (term, st, profile, service = null, label = null) {
970
+ const r = await guard(term, st, L(st).startingPairing, () => vc.startPairing({ profile, ...(service ? { service } : {}), ...(label ? { label } : {}) }))
927
971
  // `service` se pega al estado porque el daemon no lo devuelve: la pantalla del QR
928
972
  // tiene que poder decir qué se está entregando, que no es lo mismo un aparato tuyo
929
973
  // que una máquina que solo va a leer la configuración del proxy.
@@ -983,7 +1027,7 @@ async function onKeyPairMode (term, st, key) {
983
1027
  if (key.name === 'escape' || ch === 'b') { st.screen = 'devices'; return true }
984
1028
  if (key.name !== 'enter' || !cur) return true
985
1029
 
986
- if (cur.mode === 'here') { await beginPairing(term, st, activeId(st)); return true }
1030
+ if (cur.mode === 'here') { return beginPairingNamed(term, st, activeId(st)) }
987
1031
 
988
1032
  // SERVICIO: entra a la cuenta activa, pero con un certificado que solo sirve para
989
1033
  // pedir las variables de SU namespace. El nombre del ns es el que luego pide el
@@ -1025,8 +1069,10 @@ async function onKeyPairMode (term, st, key) {
1025
1069
  // Si el emparejamiento no llega a abrirse, la cuenta que se creó PARA él se va con
1026
1070
  // él: si no, cada intento fallido dejaba una cuenta vacía —y encima activa— que
1027
1071
  // luego había que ir a borrar a mano adivinando cuál era.
1028
- if (!await beginPairing(term, st, created)) await descartarCuenta(term, st, created, previa)
1029
- else st.pairing.born = { id: created, from: previa }
1072
+ beginPairingNamed(term, st, created, null, async (ok) => {
1073
+ if (!ok) await descartarCuenta(term, st, created, previa)
1074
+ else st.pairing.born = { id: created, from: previa }
1075
+ })
1030
1076
  },
1031
1077
  onCancel: () => { st.input = null }
1032
1078
  })
package/src/tui/i18n.js CHANGED
@@ -46,14 +46,22 @@ const es = {
46
46
  store: 'Guardar tus datos',
47
47
  read: 'Leer tus datos',
48
48
  admin: 'Administrar el perfil',
49
- passwords: 'Pedir tus contraseñas'
49
+ passwords: 'Pedir tus contraseñas',
50
+ approve: 'Aprobar pedidos',
51
+ sealer: 'Sellar el acta'
50
52
  },
51
53
  capHint: {
52
54
  sign: 'usar tu identidad en las apps del ecosistema',
53
55
  store: 'escribir en tu bóveda (perfil, contenido, datos sensibles)',
54
56
  read: 'ver lo que guardaste',
55
57
  admin: 'conectar y quitar dispositivos desde ese aparato, sin venir aquí',
56
- passwords: 'el gestor: pedir una credencial por sitio, nunca la lista entera'
58
+ passwords: 'el gestor: pedir una credencial por sitio, nunca la lista entera',
59
+ approve: 'dar el visto bueno cuando otro aparato pide claves privadas',
60
+ // Sellar solo significa algo en un aparato que HAGA de bóveda (otra máquina con el
61
+ // daemon, o un navegador que se puso de bóveda). En un teléfono normal se concede y no
62
+ // lo usa nadie. Se enseña igual, y se dice: verlo desactivado explica el modelo;
63
+ // esconderlo hacía creer que no existe (dueño, 2026-08-31).
64
+ sealer: 'admitir aparatos y cambiar permisos él solo, si esta bóveda no está. Solo sirve en otra BÓVEDA'
57
65
  },
58
66
  confirmAdmin: (id) => `¿Dejar que ${id} conecte y quite dispositivos sin venir aquí?`,
59
67
  capGiven: (n) => `Concedido: ${n}`,
@@ -140,6 +148,9 @@ const es = {
140
148
  cantRemoveMaster: 'Esta bóveda es la que manda: no se quita a sí misma.',
141
149
  revokedCount: (n) => ` Revocados: ${n}`,
142
150
  startingPairing: 'Iniciando emparejamiento…',
151
+ deviceSince: 'desde',
152
+ nameDeviceLabel: '¿Cómo se va a llamar este aparato?',
153
+ nameDeviceHint: 'Enter para dejar que lo diga él (usará tu apodo, y así no se distinguen)',
143
154
  noPending: 'No hay ningún dispositivo pendiente',
144
155
  noPendingToReject: 'No hay ningún dispositivo pendiente para rechazar',
145
156
  rejecting: 'Rechazando…',
@@ -345,14 +356,18 @@ const en = {
345
356
  store: 'Save your data',
346
357
  read: 'Read your data',
347
358
  admin: 'Manage the profile',
348
- passwords: 'Ask for your passwords'
359
+ passwords: 'Ask for your passwords',
360
+ approve: 'Approve requests',
361
+ sealer: 'Seal the record'
349
362
  },
350
363
  capHint: {
351
364
  sign: 'use your identity across the ecosystem apps',
352
365
  store: 'write to your vault (profile, content, sensitive data)',
353
366
  read: 'see what you saved',
354
367
  admin: 'connect and remove devices from that device, without coming here',
355
- passwords: 'the manager: ask for one credential per site, never the whole list'
368
+ passwords: 'the manager: ask for one credential per site, never the whole list',
369
+ approve: 'give the go-ahead when another device asks for private keys',
370
+ sealer: 'admit devices and change permissions on its own if this vault is gone. Only useful on another VAULT'
356
371
  },
357
372
  confirmAdmin: (id) => `Let ${id} connect and remove devices without coming here?`,
358
373
  capGiven: (n) => `Granted: ${n}`,
@@ -435,6 +450,9 @@ const en = {
435
450
  cantRemoveMaster: 'This vault is the Master: it does not remove itself.',
436
451
  revokedCount: (n) => ` Revoked: ${n}`,
437
452
  startingPairing: 'Starting pairing…',
453
+ deviceSince: 'since',
454
+ nameDeviceLabel: 'What will this device be called?',
455
+ nameDeviceHint: 'Enter to let it say (it will use your nickname, and then they all look alike)',
438
456
  noPending: 'No device is waiting',
439
457
  noPendingToReject: 'No device is waiting to be rejected',
440
458
  rejecting: 'Rejecting…',
@@ -604,10 +604,10 @@ export function pairUrl (qr) {
604
604
  return { url: inviteUrl(qr), code, payload: JSON.stringify(qr), b64: code }
605
605
  }
606
606
 
607
- export async function startPairing ({ profile, service } = {}) {
607
+ export async function startPairing ({ profile, service, label } = {}) {
608
608
  requireAlive()
609
609
  rm(F.pair); rm(F.pending)
610
- await writeReq(F.pairReq, service ? { service } : {}, profile)
610
+ await writeReq(F.pairReq, { ...(service ? { service } : {}), ...(label ? { label } : {}) }, profile)
611
611
  signalOrCleanup('SIGUSR1', [F.pairReq])
612
612
  for (let i = 0; i < 50; i++) {
613
613
  await sleep(100)