@dotrino/vaultd 0.15.0 → 0.17.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.15.0",
3
+ "version": "0.17.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
@@ -257,18 +257,14 @@ function cmdReject (deviceId) {
257
257
  * Distinto de `members` (quién es del perfil) y de `profile` (los perfiles del PC): esto
258
258
  * es el CONTENIDO, no la identidad.
259
259
  *
260
- * La foto no se imprime es un data-URI de hasta ~90 KB— sino que se resume;
261
- * `me --foto <archivo>` la escribe en disco para poder mirarla.
260
+ * De la foto solo se dice que la hay, de qué tipo y cuánto pesa: es un data-URI de hasta
261
+ * ~90 KB y una terminal no es sitio para volcarlo.
262
262
  */
263
- async function cmdMe (args = []) {
264
- const i = args.findIndex((a) => a === '--foto' || a === '--photo')
265
- const avatarPath = i >= 0 ? args[i + 1] : null
266
- if (i >= 0 && !avatarPath) { console.error('uso: dotrino-vault me --foto <archivo>'); process.exit(2) }
267
-
263
+ async function cmdMe () {
268
264
  const s = requireDaemon()
269
265
  const meFile = path.join(dir, 'me.json')
270
266
  try { fs.rmSync(meFile, { force: true }) } catch (_) {}
271
- writeReq('me-request.json', { ...(avatarPath ? { avatarPath: path.resolve(avatarPath) } : {}) })
267
+ writeReq('me-request.json', {})
272
268
  avisar(s.pid, 'SIGUSR2')
273
269
  let dump = null
274
270
  for (let n = 0; n < 50; n++) { await sleep(100); const d = readJson(meFile, null); if (d?.at) { dump = d; break } }
@@ -306,8 +302,6 @@ async function cmdMe (args = []) {
306
302
  for (const x of lista) console.log(' %s %s%s', (x.type || x.label || '').padEnd(12), x.value, x.visible === false ? ' (oculto)' : '')
307
303
  }
308
304
 
309
- if (dump.avatarGuardada) console.log('\n Foto escrita en: %s', dump.avatarGuardada)
310
- else if (me.avatar) console.log('\n Para verla: dotrino-vault me --foto ~/perfil.png')
311
305
  console.log('')
312
306
  }
313
307
 
@@ -348,6 +342,40 @@ async function cmdMembers () {
348
342
  console.log(' Los servicios solo pueden abrir las claves de su propio nombre; eso no se cambia aquí.\n')
349
343
  }
350
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
+
351
379
  /** `dotrino-vault caps <ID> ±permiso` — cambia lo que puede hacer un dispositivo. */
352
380
  async function cmdCaps (args = []) {
353
381
  const [id, ...cambios] = args
@@ -360,14 +388,7 @@ async function cmdCaps (args = []) {
360
388
  sign: 'sign', store: 'store', read: 'read', admin: 'admin'
361
389
  }
362
390
  const s = requireDaemon()
363
- const actaFile = path.join(dataDir(), 'acta.json')
364
- try { fs.rmSync(actaFile, { force: true }) } catch (_) {}
365
- writeReq('dump-request.json', {})
366
- avisar(s.pid, 'SIGUSR2')
367
- let acta = null
368
- for (let i = 0; i < 50; i++) { await sleep(100); const a = readJson(actaFile, null); if (a?.at) { acta = a; break } }
369
- const m = acta?.members?.find((x) => x.id === id.toUpperCase())
370
- 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)
371
392
 
372
393
  const caps = new Set(m.caps)
373
394
  for (const c of cambios) {
@@ -648,6 +669,7 @@ function help () {
648
669
  devices lista dispositivos enrolados / revocados
649
670
  me tu perfil (nombre, foto, datos) tal como lo tiene la bóveda
650
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)
651
673
  caps <ID> ±permiso cambia permisos (+firma -guarda +administra …)
652
674
  revoke <nonce> revoca un dispositivo (le ordena autoborrarse)
653
675
  activity [n] bitácora de seguridad: firmas, renovaciones, enrolados, rechazos
@@ -691,8 +713,9 @@ export async function runCtl (argv) {
691
713
  case 'approve': return cmdApprove(rest[0])
692
714
  case 'reject': return cmdReject(rest[0])
693
715
  case 'devices': return cmdDevices()
694
- case 'me': return cmdMe(rest)
716
+ case 'me': return cmdMe()
695
717
  case 'members': return cmdMembers()
718
+ case 'label': return cmdLabel(rest)
696
719
  case 'caps': return cmdCaps(rest)
697
720
  case 'revoke': return cmdRevoke(rest[0])
698
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'))
@@ -257,7 +266,7 @@ export async function runDaemon () {
257
266
  // Solo se vuelca cuando se PIDE, no en cada señal: es contenido del usuario y no tiene
258
267
  // por qué quedar escrito en un archivo suelto cada vez que alguien mira los miembros.
259
268
  // La FOTO no entra en el volcado (son hasta ~90 KB de data-URI que nadie va a leer en
260
- // una terminal): se resume, y si la quieres, `--foto <archivo>` la escribe donde digas.
269
+ // una terminal): solo se dice que la hay, de qué tipo y cuánto pesa.
261
270
  const meReq = readJsonSafe(meReqFile)
262
271
  if (meReq) {
263
272
  rm(meReqFile)
@@ -265,15 +274,7 @@ export async function runDaemon () {
265
274
  const tm = resolveTarget(meReq) || { id: mgr.currentId(), vault: mgr.current() }
266
275
  const { me } = tm.vault.threads.methods.profileGet()
267
276
  const { avatar, ...resto } = me || {}
268
- let guardada = null
269
- if (meReq.avatarPath && typeof avatar === 'string') {
270
- const m = /^data:([^;,]+)?(?:;base64)?,(.*)$/s.exec(avatar)
271
- if (m) {
272
- fs.writeFileSync(meReq.avatarPath, Buffer.from(m[2], 'base64'), { mode: 0o600 })
273
- guardada = meReq.avatarPath
274
- }
275
- }
276
- writeJson(meFile, { v: 1, at: Date.now(), profile: tm.id, me: me ? { ...resto, avatar: avatarInfo(avatar) } : null, avatarGuardada: guardada })
277
+ writeJson(meFile, { v: 1, at: Date.now(), profile: tm.id, me: me ? { ...resto, avatar: avatarInfo(avatar) } : null })
277
278
  } catch (e) { console.error('[vault] could not dump the profile:', e.message) }
278
279
  }
279
280
  } catch (e) {
package/src/tui/app.js CHANGED
@@ -268,7 +268,6 @@ function meRows (st, t) {
268
268
  rows.push({ text: t.accent(' ▸ ' + titulo), sel: false })
269
269
  for (const x of lista) campo(x.type || x.label || '', x.value, x.visible === false)
270
270
  }
271
- if (me.avatar) { rows.push({ text: '', sel: false }); rows.push({ text: t.muted(i.savePhotoHint), sel: false }) }
272
271
  return rows
273
272
  }
274
273
 
@@ -499,6 +498,20 @@ async function onKeyDevices (term, st, key) {
499
498
  },
500
499
  onNo: () => { st.confirm = null }
501
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
+ })
502
515
  } else if (ch === 'r') {
503
516
  await refreshDevices(term, st)
504
517
  }
@@ -603,27 +616,11 @@ async function onKeyPairing (term, st, key) {
603
616
  }
604
617
 
605
618
  /**
606
- * Teclas del PERFIL: refrescar y guardar la foto. Nada de editar — el perfil se edita en
607
- * el dispositivo que usas, no en la máquina donde vive la bóveda.
619
+ * Teclas del PERFIL: solo refrescar. Nada de editar — el perfil se edita en el dispositivo
620
+ * que usas, no en la máquina donde vive la bóveda.
608
621
  */
609
622
  async function onKeyMe (term, st, key) {
610
- const i = L(st)
611
- const ch = key.name === 'char' ? key.ch.toLowerCase() : null
612
- if (ch === 'r') {
613
- await refreshMe(term, st)
614
- } else if (ch === 'f' && st.me?.avatar) {
615
- setInput(st, {
616
- label: i.savePhotoLabel,
617
- hint: i.savePhotoHintInput,
618
- value: 'perfil.png',
619
- onSubmit: async (valor) => {
620
- const destino = String(valor || '').trim()
621
- if (!destino) return
622
- const r = await guard(term, st, i.savingPhoto, () => vc.saveAvatar(destino, activeId(st)))
623
- if (r.ok && r.v) flash(st, i.photoSaved(r.v))
624
- }
625
- })
626
- }
623
+ if (key.name === 'char' && key.ch.toLowerCase() === 'r') await refreshMe(term, st)
627
624
  return true
628
625
  }
629
626
 
package/src/tui/i18n.js CHANGED
@@ -53,12 +53,7 @@ const es = {
53
53
  hidden: ' (oculto)',
54
54
  noName: '(sin nombre)',
55
55
  no: 'no',
56
- savePhotoHint: ' Pulsa F para guardar la foto y poder verla.',
57
- savePhotoLabel: '¿Dónde guardo la foto?',
58
- savePhotoHintInput: 'ruta del archivo (Esc cancela)',
59
- savingPhoto: 'Guardando la foto…',
60
- photoSaved: (ruta) => `Foto guardada en ${ruta}`,
61
- helpMe: ['←→ pestaña', 'r refrescar', 'f guardar foto', 'Esc bóvedas', 'l English', 'q salir'],
56
+ helpMe: ['←→ pestaña', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
62
57
  tabsHint: ' (←→ cambiar)',
63
58
  titleProfiles: 'Bóvedas',
64
59
  titlePairing: 'Emparejar un dispositivo',
@@ -180,7 +175,11 @@ const es = {
180
175
  // language/quit); lo único que se traduce es la palabra que las explica.
181
176
  // Segmentos, no una línea: el render recorta del medio si no caben.
182
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'],
183
- helpDevices: ['←→ pestaña', '↑↓', 'p emparejar', 'a aprobar', 'x rechazar', 'v revocar', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
178
+ helpDevices: ['←→ pestaña', '↑↓', 'p emparejar', 'a aprobar', 'x rechazar', 'n renombrar', 'v revocar', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
179
+ renameDeviceLabel: (id) => `¿Cómo quieres llamar a ${id}?`,
180
+ renameDeviceHint: 'el nombre con el que lo reconoces (Esc cancela)',
181
+ renaming: 'Renombrando…',
182
+ deviceRenamed: (n) => `Ahora se llama «${n}»`,
184
183
  helpSecrets: ['←→ pestaña', '↑↓', 'n nueva variable', 'x quitar (variable/scope)', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
185
184
  helpPairing: ['a aprobar', 'x rechazar', 'r reiniciar', '↑↓ scroll', 'Esc atrás', 'l English'],
186
185
  helpPairMode: ['↑↓', 'Enter elegir', 'Esc atrás', 'l English', 'q salir'],
@@ -245,12 +244,7 @@ const en = {
245
244
  hidden: ' (hidden)',
246
245
  noName: '(no name)',
247
246
  no: 'no',
248
- savePhotoHint: ' Press F to save the photo so you can look at it.',
249
- savePhotoLabel: 'Where should I save the photo?',
250
- savePhotoHintInput: 'file path (Esc cancels)',
251
- savingPhoto: 'Saving the photo…',
252
- photoSaved: (ruta) => `Photo saved to ${ruta}`,
253
- helpMe: ['←→ tab', 'r refresh', 'f save photo', 'Esc vaults', 'l Español', 'q quit'],
247
+ helpMe: ['←→ tab', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
254
248
  tabsHint: ' (←→ switch)',
255
249
  titleProfiles: 'Vaults',
256
250
  titlePairing: 'Pair a device',
@@ -361,7 +355,11 @@ const en = {
361
355
  helpConfirm: 'y confirm · n/Esc cancel',
362
356
 
363
357
  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'],
364
- helpDevices: ['←→ tab', '↑↓', 'p pair', 'a approve', 'x reject', 'v revoke', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
358
+ helpDevices: ['←→ tab', '↑↓', 'p pair', 'a approve', 'x reject', 'n rename', 'v revoke', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
359
+ renameDeviceLabel: (id) => `What do you want to call ${id}?`,
360
+ renameDeviceHint: 'the name you recognise it by (Esc cancels)',
361
+ renaming: 'Renaming…',
362
+ deviceRenamed: (n) => `Now called "${n}"`,
365
363
  helpSecrets: ['←→ tab', '↑↓', 'n new variable', 'x remove (variable/scope)', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
366
364
  helpPairing: ['a approve', 'x reject', 'r restart', '↑↓ scroll', 'Esc back', 'l Español'],
367
365
  helpPairMode: ['↑↓', 'Enter choose', 'Esc back', 'l Español', 'q quit'],
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()
@@ -234,17 +247,6 @@ export async function getMe (profile) {
234
247
  return d.me || null
235
248
  }
236
249
 
237
- /** Escribe la foto de perfil en `destino` (para poder mirarla) y devuelve la ruta. */
238
- export async function saveAvatar (destino, profile) {
239
- requireAlive()
240
- rm(F.me)
241
- writeReq(F.meReq, { avatarPath: destino }, profile)
242
- signalOrCleanup('SIGUSR2', [F.meReq])
243
- const d = await waitFor(F.me)
244
- rm(F.me)
245
- if (!d) throw coded('the daemon did not reply', 'NO_REPLY')
246
- return d.avatarGuardada || null
247
- }
248
250
 
249
251
  // ---------------------------------------------------------------------------
250
252
  // Secretos: scopes (namespaces) y variables (claves)