@dotrino/vaultd 0.16.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 +2 -2
- package/src/ctl.js +37 -8
- package/src/daemon.js +9 -0
- package/src/tui/app.js +14 -0
- package/src/tui/i18n.js +10 -2
- package/src/vault.js +9 -0
- package/src/vaultControl.js +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "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.
|
|
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
|
|
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
|
}
|
package/src/tui/i18n.js
CHANGED
|
@@ -175,7 +175,11 @@ 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'],
|
|
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}»`,
|
|
179
183
|
helpSecrets: ['←→ pestaña', '↑↓', 'n nueva variable', 'x quitar (variable/scope)', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
|
|
180
184
|
helpPairing: ['a aprobar', 'x rechazar', 'r reiniciar', '↑↓ scroll', 'Esc atrás', 'l English'],
|
|
181
185
|
helpPairMode: ['↑↓', 'Enter elegir', 'Esc atrás', 'l English', 'q salir'],
|
|
@@ -351,7 +355,11 @@ const en = {
|
|
|
351
355
|
helpConfirm: 'y confirm · n/Esc cancel',
|
|
352
356
|
|
|
353
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'],
|
|
354
|
-
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}"`,
|
|
355
363
|
helpSecrets: ['←→ tab', '↑↓', 'n new variable', 'x remove (variable/scope)', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
|
|
356
364
|
helpPairing: ['a approve', 'x reject', 'r restart', '↑↓ scroll', 'Esc back', 'l Español'],
|
|
357
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' })
|
package/src/vaultControl.js
CHANGED
|
@@ -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()
|