@dotrino/vaultd 0.16.0 → 0.18.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.16.0",
3
+ "version": "0.18.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": {
@@ -19,7 +19,7 @@
19
19
  "node": ">=20"
20
20
  },
21
21
  "dependencies": {
22
- "@dotrino/identity": "^0.38.0",
22
+ "@dotrino/identity": "^0.39.0",
23
23
  "@dotrino/proxy-client": "^0.10.0",
24
24
  "ws": "^8.18.0"
25
25
  },
package/src/ctl.js CHANGED
@@ -342,6 +342,40 @@ async function cmdMembers () {
342
342
  console.log(' Los servicios solo pueden abrir las claves de su propio nombre; eso no se cambia aquí.\n')
343
343
  }
344
344
 
345
+ /**
346
+ * `dotrino-vault label <ID> <nombre>` — renombra un dispositivo.
347
+ *
348
+ * El nombre lo pone el aparato al emparejarse (y si no le diste uno, entra con TU apodo de
349
+ * ese momento), así que se queda desfasado en cuanto te renombras. Esto lo arregla sin
350
+ * tener que revocar y volver a emparejar.
351
+ */
352
+ async function cmdLabel (args = []) {
353
+ const [id, ...resto] = args
354
+ const nombre = resto.join(' ').trim()
355
+ if (!id || !nombre) {
356
+ console.error('uso: dotrino-vault label <ID> <nombre> (p.ej. label AB12-CD34 "Teléfono de casa")')
357
+ process.exit(2)
358
+ }
359
+ const m = await buscarMiembro(id)
360
+ writeReq('label-request.json', { pub: m.pub, label: nombre })
361
+ avisar(requireDaemon().pid, 'SIGUSR2')
362
+ console.log('Listo: %s ahora se llama «%s». Compruébalo con: dotrino-vault members', m.id, nombre.slice(0, 60))
363
+ }
364
+
365
+ /** Busca un miembro del acta por su identificador (AB12-CD34) o se rinde con un mensaje claro. */
366
+ async function buscarMiembro (id) {
367
+ const s = requireDaemon()
368
+ const actaFile = path.join(dataDir(), 'acta.json')
369
+ try { fs.rmSync(actaFile, { force: true }) } catch (_) {}
370
+ writeReq('dump-request.json', {})
371
+ avisar(s.pid, 'SIGUSR2')
372
+ let acta = null
373
+ for (let i = 0; i < 50; i++) { await sleep(100); const a = readJson(actaFile, null); if (a?.at) { acta = a; break } }
374
+ const m = acta?.members?.find((x) => x.id === String(id).toUpperCase())
375
+ if (!m) { console.error('No hay ningún dispositivo con ese identificador. Míralos con: dotrino-vault members'); process.exit(1) }
376
+ return m
377
+ }
378
+
345
379
  /** `dotrino-vault caps <ID> ±permiso` — cambia lo que puede hacer un dispositivo. */
346
380
  async function cmdCaps (args = []) {
347
381
  const [id, ...cambios] = args
@@ -354,14 +388,7 @@ async function cmdCaps (args = []) {
354
388
  sign: 'sign', store: 'store', read: 'read', admin: 'admin'
355
389
  }
356
390
  const s = requireDaemon()
357
- const actaFile = path.join(dataDir(), 'acta.json')
358
- try { fs.rmSync(actaFile, { force: true }) } catch (_) {}
359
- writeReq('dump-request.json', {})
360
- avisar(s.pid, 'SIGUSR2')
361
- let acta = null
362
- for (let i = 0; i < 50; i++) { await sleep(100); const a = readJson(actaFile, null); if (a?.at) { acta = a; break } }
363
- const m = acta?.members?.find((x) => x.id === id.toUpperCase())
364
- if (!m) { console.error('No hay ningún dispositivo con ese identificador. Míralos con: dotrino-vault members'); process.exit(1) }
391
+ const m = await buscarMiembro(id)
365
392
 
366
393
  const caps = new Set(m.caps)
367
394
  for (const c of cambios) {
@@ -642,6 +669,7 @@ function help () {
642
669
  devices lista dispositivos enrolados / revocados
643
670
  me tu perfil (nombre, foto, datos) tal como lo tiene la bóveda
644
671
  members el acta del perfil: quién es tuyo y qué puede hacer
672
+ label <ID> <nombre> renombra un dispositivo (el nombre con el que lo reconoces)
645
673
  caps <ID> ±permiso cambia permisos (+firma -guarda +administra …)
646
674
  revoke <nonce> revoca un dispositivo (le ordena autoborrarse)
647
675
  activity [n] bitácora de seguridad: firmas, renovaciones, enrolados, rechazos
@@ -687,6 +715,7 @@ export async function runCtl (argv) {
687
715
  case 'devices': return cmdDevices()
688
716
  case 'me': return cmdMe()
689
717
  case 'members': return cmdMembers()
718
+ case 'label': return cmdLabel(rest)
690
719
  case 'caps': return cmdCaps(rest)
691
720
  case 'revoke': return cmdRevoke(rest[0])
692
721
  case 'secret': return cmdSecret(rest)
package/src/daemon.js CHANGED
@@ -205,6 +205,15 @@ export async function runDaemon () {
205
205
  rm(rejectReqFile)
206
206
  }
207
207
  // Cambio de permisos de un miembro del acta (`dotrino-vault caps`).
208
+ // Renombrar un dispositivo (`dotrino-vault label <ID> <nombre>`).
209
+ const labelReq = readJsonSafe(path.join(dir, 'label-request.json'))
210
+ if (labelReq?.pub && typeof labelReq.label === 'string') {
211
+ rm(path.join(dir, 'label-request.json'))
212
+ try {
213
+ await targetOf(labelReq)?.setLabel(labelReq.pub, labelReq.label)
214
+ console.log('[vault] device renamed: %s', labelReq.label || '(no name)')
215
+ } catch (e) { console.error('[vault] could not rename the device:', e.message) }
216
+ }
208
217
  const capsReq = readJsonSafe(path.join(dir, 'caps-request.json'))
209
218
  if (capsReq?.pub && Array.isArray(capsReq.caps)) {
210
219
  rm(path.join(dir, 'caps-request.json'))
package/src/tui/app.js CHANGED
@@ -498,6 +498,20 @@ async function onKeyDevices (term, st, key) {
498
498
  },
499
499
  onNo: () => { st.confirm = null }
500
500
  })
501
+ } else if (ch === 'n' && cur) {
502
+ // Renombrar: el nombre lo trae el aparato al emparejarse (y si no le diste uno, entra
503
+ // con TU apodo de ese momento), así que a la semana ya no dice nada.
504
+ setInput(st, {
505
+ label: i.renameDeviceLabel(cur.deviceId),
506
+ hint: i.renameDeviceHint,
507
+ value: cur.label || '',
508
+ onSubmit: async (valor) => {
509
+ const nombre = String(valor || '').trim()
510
+ if (!nombre) return
511
+ const r = await guard(term, st, i.renaming, () => vc.setDeviceLabel(cur.sub, nombre, activeId(st)))
512
+ if (r.ok) { st.devices = r.v; flash(st, i.deviceRenamed(nombre)) }
513
+ }
514
+ })
501
515
  } else if (ch === 'r') {
502
516
  await refreshDevices(term, st)
503
517
  }
@@ -715,14 +729,27 @@ async function onConfirmKey (st, key) {
715
729
  const INNER_TABS = ['devices', 'secrets', 'me']
716
730
  const tabLabel = (i, k) => ({ devices: i.tabDevices, secrets: i.tabSecrets, me: i.tabMe })[k]
717
731
 
718
- const helpSegs = (i, screen) => ({
719
- profiles: i.helpProfiles,
720
- devices: i.helpDevices,
721
- secrets: i.helpSecrets,
722
- pairing: i.helpPairing,
723
- pairmode: i.helpPairMode,
724
- me: i.helpMe
725
- })[screen] || []
732
+ /**
733
+ * Las teclas que se pueden usar AHORA, no el catálogo entero. Aprobar/rechazar sin nadie
734
+ * esperando, o revocar sin un aparato seleccionado, no hacen nada: anunciarlas confunde
735
+ * («¿por qué no pasa nada?») y además quema esas letras para otros usos en la pantalla.
736
+ */
737
+ const helpSegs = (i, screen, st = {}) => {
738
+ const segs = {
739
+ profiles: i.helpProfiles,
740
+ devices: i.helpDevices,
741
+ secrets: i.helpSecrets,
742
+ pairing: i.helpPairing,
743
+ pairmode: i.helpPairMode,
744
+ me: i.helpMe
745
+ }[screen] || []
746
+ if (typeof segs !== 'function') return segs
747
+ return segs({
748
+ pendiente: !!st.pending,
749
+ hayAparatos: (st.devices?.issued || []).length > 0,
750
+ haySecretos: Object.keys(st.secrets || {}).length > 0
751
+ })
752
+ }
726
753
 
727
754
  const title = (i, screen) => ({
728
755
  profiles: i.titleProfiles,
@@ -837,7 +864,7 @@ function render (term, st) {
837
864
  } else lines[statusRow] = ''
838
865
 
839
866
  // barra de ayuda
840
- let help = fitHelp(helpSegs(i, st.screen), cols)
867
+ let help = fitHelp(helpSegs(i, st.screen, st), cols)
841
868
  if (st.input) help = i.helpInput
842
869
  else if (st.confirm) help = i.helpConfirm
843
870
  lines[rows - 1] = t.bar(help, cols)
package/src/tui/i18n.js CHANGED
@@ -175,8 +175,24 @@ const es = {
175
175
  // language/quit); lo único que se traduce es la palabra que las explica.
176
176
  // Segmentos, no una línea: el render recorta del medio si no caben.
177
177
  helpProfiles: ['↑↓', 'Enter entrar', 'p emparejar', 'n nueva', 'r renombrar', 'd borrar', 'c clave', 'x quitar-clave', 'u desbloq', 'k bloquear', 'l English', 'q salir'],
178
- helpDevices: ['←→ pestaña', '↑↓', 'p emparejar', 'a aprobar', 'x rechazar', 'v revocar', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
179
- helpSecrets: ['←→ pestaña', '↑↓', 'n nueva variable', 'x quitar (variable/scope)', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
178
+ // La barra dice lo que se PUEDE hacer AHORA, no todo lo que existe: aprobar/rechazar solo
179
+ // valen si hay alguien esperando, y renombrar/revocar solo si hay un aparato seleccionado.
180
+ // Anunciar teclas muertas confunde y además las quema para otros usos.
181
+ helpDevices: ({ pendiente, hayAparatos } = {}) => [
182
+ '←→ pestaña', '↑↓', 'p emparejar',
183
+ ...(pendiente ? ['a aprobar', 'x rechazar'] : []),
184
+ ...(hayAparatos ? ['n renombrar', 'v revocar'] : []),
185
+ 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'
186
+ ],
187
+ renameDeviceLabel: (id) => `¿Cómo quieres llamar a ${id}?`,
188
+ renameDeviceHint: 'el nombre con el que lo reconoces (Esc cancela)',
189
+ renaming: 'Renombrando…',
190
+ deviceRenamed: (n) => `Ahora se llama «${n}»`,
191
+ helpSecrets: ({ haySecretos } = {}) => [
192
+ '←→ pestaña', '↑↓', 'n nueva variable',
193
+ ...(haySecretos ? ['x quitar (variable/scope)'] : []),
194
+ 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'
195
+ ],
180
196
  helpPairing: ['a aprobar', 'x rechazar', 'r reiniciar', '↑↓ scroll', 'Esc atrás', 'l English'],
181
197
  helpPairMode: ['↑↓', 'Enter elegir', 'Esc atrás', 'l English', 'q salir'],
182
198
 
@@ -351,8 +367,21 @@ const en = {
351
367
  helpConfirm: 'y confirm · n/Esc cancel',
352
368
 
353
369
  helpProfiles: ['↑↓', 'Enter open', 'p pair', 'n new', 'r rename', 'd delete', 'c password', 'x drop-password', 'u unlock', 'k lock', 'l Español', 'q quit'],
354
- helpDevices: ['←→ tab', '↑↓', 'p pair', 'a approve', 'x reject', 'v revoke', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
355
- helpSecrets: ['←→ tab', '↑↓', 'n new variable', 'x remove (variable/scope)', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
370
+ helpDevices: ({ pendiente, hayAparatos } = {}) => [
371
+ '←→ tab', '↑↓', 'p pair',
372
+ ...(pendiente ? ['a approve', 'x reject'] : []),
373
+ ...(hayAparatos ? ['n rename', 'v revoke'] : []),
374
+ 'r refresh', 'Esc vaults', 'l Español', 'q quit'
375
+ ],
376
+ renameDeviceLabel: (id) => `What do you want to call ${id}?`,
377
+ renameDeviceHint: 'the name you recognise it by (Esc cancels)',
378
+ renaming: 'Renaming…',
379
+ deviceRenamed: (n) => `Now called "${n}"`,
380
+ helpSecrets: ({ haySecretos } = {}) => [
381
+ '←→ tab', '↑↓', 'n new variable',
382
+ ...(haySecretos ? ['x remove (variable/scope)'] : []),
383
+ 'r refresh', 'Esc vaults', 'l Español', 'q quit'
384
+ ],
356
385
  helpPairing: ['a approve', 'x reject', 'r restart', '↑↓ scroll', 'Esc back', 'l Español'],
357
386
  helpPairMode: ['↑↓', 'Enter choose', 'Esc back', 'l Español', 'q quit'],
358
387
 
package/src/vault.js CHANGED
@@ -489,6 +489,15 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
489
489
  await notifyMembers('caps', { deviceId: await deviceIdOf(pub).catch(() => null), caps })
490
490
  return r
491
491
  },
492
+ // Renombrar un dispositivo: es un nombre para el humano (no toca permisos ni llaves),
493
+ // pero pasa por el acta y se avisa, para que el cambio no sea invisible en el resto.
494
+ setLabel: async (pub, label) => {
495
+ const r = await identity.setLabel(pub, label)
496
+ const device = await deviceIdOf(pub).catch(() => null)
497
+ audit('label', { device, label: r?.label ?? label })
498
+ await notifyMembers('label', { deviceId: device, label: r?.label ?? label })
499
+ return r
500
+ },
492
501
  revokeDevice: async (nonce) => {
493
502
  const r = await desk.revoke(nonce)
494
503
  await notifyMembers('revoked', { certNonce: nonce, by: 'pc' })
@@ -38,6 +38,7 @@ const F = {
38
38
  approveReq: 'approve-request.json',
39
39
  rejectReq: 'reject-request.json',
40
40
  revokeReq: 'revoke-request.json',
41
+ labelReq: 'label-request.json',
41
42
  secretReq: 'secret-request.json',
42
43
  profileReq: 'profile-request.json',
43
44
  meReq: 'me-request.json',
@@ -199,6 +200,18 @@ export async function listDevices (profile) {
199
200
  return { issued: withIds, revoked: devices.revoked || [], profile: devices.profile || null }
200
201
  }
201
202
 
203
+ /**
204
+ * Renombra un dispositivo. El nombre lo trae el aparato al emparejarse (o, si no le diste
205
+ * ninguno, el apodo que tuvieras ese día), así que se queda desfasado enseguida.
206
+ */
207
+ export async function setDeviceLabel (pub, label, profile) {
208
+ requireAlive()
209
+ writeReq(F.labelReq, { pub, label }, profile)
210
+ signalOrCleanup('SIGUSR2', [F.labelReq])
211
+ await sleep(400)
212
+ return listDevices(profile)
213
+ }
214
+
202
215
  /** Revoca un dispositivo por su `nonce` (le ordena autoborrarse) y revuelca. */
203
216
  export async function revokeDevice (nonce, profile) {
204
217
  requireAlive()