@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 +2 -2
- package/src/ctl.js +37 -8
- package/src/daemon.js +9 -0
- package/src/tui/app.js +36 -9
- package/src/tui/i18n.js +33 -4
- 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.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.
|
|
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
|
}
|
|
@@ -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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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:
|
|
355
|
-
|
|
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' })
|
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()
|