@dotrino/vaultd 0.142.0 → 0.144.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/bin/dotrino-vaultd.js +10 -0
- package/package.json +2 -2
- package/src/tui/app.js +31 -5
- package/src/tui/i18n.js +19 -4
- package/src/vaultControl.js +24 -0
package/bin/dotrino-vaultd.js
CHANGED
|
@@ -59,6 +59,16 @@ try {
|
|
|
59
59
|
throw e
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// POR DÓNDE VA EL TRÁFICO, AL LOG. La bóveda tiene el canal directo encendido para los
|
|
63
|
+
// aparatos del acta, y sin esto no había forma de ver si lo usan o si todo sigue dando la
|
|
64
|
+
// vuelta por el proxio. Una vez por PROCESO y no por perfil: `logStats` lee todos los
|
|
65
|
+
// clientes que haya (uno por perfil abierto). Escribe con `console.log` en el momento, así
|
|
66
|
+
// que con `--tui` cae en el mismo búfer que el resto y no pinta encima de la pantalla.
|
|
67
|
+
{
|
|
68
|
+
const { logStats } = await import('@dotrino/proxy-client')
|
|
69
|
+
logStats({ label: 'vault' })
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
// Atajo de dev: --pair imprime el QR directo en stdout (en producción se usa el CLI).
|
|
63
73
|
// Empareja contra el perfil ACTIVO.
|
|
64
74
|
if (process.argv.includes('--pair')) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.144.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": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@dotrino/identity": "^0.107.1",
|
|
26
26
|
"@dotrino/opaque": "0.1.0",
|
|
27
27
|
"@dotrino/passmanager": "0.17.0",
|
|
28
|
-
"@dotrino/proxy-client": "0.
|
|
28
|
+
"@dotrino/proxy-client": "0.30.0",
|
|
29
29
|
"@dotrino/roadmap": "0.3.19",
|
|
30
30
|
"@dotrino/store": "0.11.0",
|
|
31
31
|
"@dotrino/update": "0.2.2",
|
package/src/tui/app.js
CHANGED
|
@@ -160,6 +160,9 @@ function mergeMembersAndCerts (members, issued) {
|
|
|
160
160
|
})
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/** La entrada de bloqueo de un aparato (por su llave), o `null` si no está bloqueado. */
|
|
164
|
+
const blockedOf = (st, pub) => (pub && (st.blocked || []).find((b) => b.pub === pub)) || null
|
|
165
|
+
|
|
163
166
|
function activeProfile (st) {
|
|
164
167
|
const list = st.profiles?.profiles || []
|
|
165
168
|
return list.find((p) => p.current) || list[0] || null
|
|
@@ -388,11 +391,15 @@ function deviceRows (st, t) {
|
|
|
388
391
|
(debt ? t.warn(` ${i.deviceDebt(debt)}`) : '')
|
|
389
392
|
// SIN ACCESO: está en el acta y no puede entrar. Es un aviso, no un adorno, así que va
|
|
390
393
|
// en el color de aviso y en el sitio donde estaría su vencimiento.
|
|
391
|
-
|
|
394
|
+
// BLOQUEADO: sigue en el acta pero la bóveda y los agentes no le atienden. Es un
|
|
395
|
+
// aviso (lo decidió un aprobador o el dueño), así que va en el color de aviso y
|
|
396
|
+
// delante de todo lo demás: es lo primero que hay que ver de esa fila.
|
|
397
|
+
const bloq = blockedOf(st, d.sub)
|
|
398
|
+
const status = (bloq ? t.warn(i.deviceBlocked) + ' ' : '') + (d.noAccess
|
|
392
399
|
? t.warn(i.deviceNoAccess)
|
|
393
400
|
: d.isMaster
|
|
394
401
|
? t.muted(i.thisVault)
|
|
395
|
-
: t.muted('scope:' + shortScope(d.scope)) + ' ' + t.muted('exp:' + fmtExp(d.exp))
|
|
402
|
+
: t.muted('scope:' + shortScope(d.scope)) + ' ' + t.muted('exp:' + fmtExp(d.exp)))
|
|
396
403
|
const marca = conCambios.has(d.sub) ? t.bold('*') : ' '
|
|
397
404
|
rows.push({ text: `${marca}${t.bold(d.deviceId)} ${label}${desde} ${status}${extra}`, sel: true, meta: d })
|
|
398
405
|
}
|
|
@@ -909,7 +916,7 @@ async function refreshAll (term, st, api = vc) {
|
|
|
909
916
|
// Cerrada: no se pide su contenido, y tampoco se enseña un error por mirarla desde
|
|
910
917
|
// fuera. Lo que hubiera cargado se suelta, para no dejar en pantalla lo de antes.
|
|
911
918
|
if (activeLocked(st)) {
|
|
912
|
-
st.devices = null; st.secrets = null; st.members = []; st.me = undefined; st.capsDrafts = {}
|
|
919
|
+
st.devices = null; st.secrets = null; st.members = []; st.blocked = []; st.me = undefined; st.capsDrafts = {}
|
|
913
920
|
return
|
|
914
921
|
}
|
|
915
922
|
const r = await guard(term, st, L(st).loading, () => api.snapshot(activeId(st)))
|
|
@@ -920,7 +927,7 @@ async function refreshAll (term, st, api = vc) {
|
|
|
920
927
|
if (secrets) st.secrets = { ns: secrets.ns || {}, dev: Array.isArray(secrets.dev) ? secrets.dev : [] }
|
|
921
928
|
// El ACTA entra en el volcado normal: es de donde sale la lista de dispositivos (ver
|
|
922
929
|
// `mergeMembersAndCerts`). Antes solo se pedía al abrir la pantalla de permisos.
|
|
923
|
-
if (record) st.members = record.members || []
|
|
930
|
+
if (record) { st.members = record.members || []; st.blocked = Array.isArray(record.blocked) ? record.blocked : [] }
|
|
924
931
|
if (devices) {
|
|
925
932
|
const issued = (devices.issued || devices.active || devices.delegations || [])
|
|
926
933
|
st.devices = { issued: await Promise.all(issued.map(async (d) => ({ ...d, deviceId: d.sub ? await api.deviceIdOf(d.sub) : '????-????' }))), revoked: devices.revoked || [] }
|
|
@@ -939,6 +946,7 @@ async function refreshAll (term, st, api = vc) {
|
|
|
939
946
|
function applyDump (st, v) {
|
|
940
947
|
st.devices = v
|
|
941
948
|
if (Array.isArray(v?.members)) st.members = v.members
|
|
949
|
+
if (Array.isArray(v?.blocked)) st.blocked = v.blocked
|
|
942
950
|
}
|
|
943
951
|
|
|
944
952
|
async function refreshDevices (term, st) {
|
|
@@ -1257,6 +1265,20 @@ async function onKeyDevices (term, st, key) {
|
|
|
1257
1265
|
},
|
|
1258
1266
|
onNo: () => { st.confirm = null }
|
|
1259
1267
|
})
|
|
1268
|
+
} else if (ch === 'b' && cur?.isMaster) {
|
|
1269
|
+
flash(st, i.cantBlockMaster, 'warn')
|
|
1270
|
+
} else if (ch === 'b' && cur?.sub) {
|
|
1271
|
+
// BLOQUEAR / DESBLOQUEAR (dueño, 2026-10-07). No toca el acta: el aparato sigue siendo
|
|
1272
|
+
// miembro, pero la bóveda y los agentes no le atienden hasta que se le quite. Lo normal
|
|
1273
|
+
// es que bloquee el aprobador desde el teléfono al recibir un incidente; desbloquear
|
|
1274
|
+
// SOLO se hace aquí o desde la consola de administración. `b` = bloquear en Bóvedas
|
|
1275
|
+
// también (k bloquea el perfil; esto es otro candado y otra tecla, para no confundir).
|
|
1276
|
+
const on = !blockedOf(st, cur.sub)
|
|
1277
|
+
const r = await guard(term, st, on ? i.blocking : i.unblocking, () => vc.setDeviceBlocked(cur.sub, on, activeId(st)))
|
|
1278
|
+
if (r.ok) {
|
|
1279
|
+
flash(st, on ? i.deviceBlockedMsg(cur.deviceId) : (r.v.was ? i.deviceUnblockedMsg(cur.deviceId) : i.deviceNotBlocked(cur.deviceId)))
|
|
1280
|
+
await refreshDevices(term, st)
|
|
1281
|
+
}
|
|
1260
1282
|
} else if (ch === 'r' && cur) {
|
|
1261
1283
|
// Renombrar: el nombre lo trae el aparato al emparejarse (y si no le diste uno, entra
|
|
1262
1284
|
// con TU apodo de ese momento), así que a la semana ya no dice nada. `r` es renombrar
|
|
@@ -2298,8 +2320,11 @@ const helpSegs = (i, screen, st = {}) => {
|
|
|
2298
2320
|
return segs({
|
|
2299
2321
|
pending: !!st.pending,
|
|
2300
2322
|
drafts: draftChanges(st).length > 0,
|
|
2301
|
-
|
|
2323
|
+
// Las filas salen del ACTA (miembros con o sin certificado), así que la ayuda mira
|
|
2324
|
+
// la lista fundida: un miembro sin papel también se renombra, bloquea o quita.
|
|
2325
|
+
hasDevices: devs.length > 0,
|
|
2302
2326
|
isService: !!cur?.cn,
|
|
2327
|
+
curBlocked: !!blockedOf(st, cur?.sub),
|
|
2303
2328
|
hasSecrets: Object.keys(st.secrets?.ns || {}).length > 0,
|
|
2304
2329
|
hasVars: devVarsOf(st, st.varsFor?.pub).length > 0,
|
|
2305
2330
|
hasLogins: (st.logins || []).length > 0,
|
|
@@ -2528,6 +2553,7 @@ export async function runTui () {
|
|
|
2528
2553
|
scroll: {},
|
|
2529
2554
|
profiles: null,
|
|
2530
2555
|
devices: null,
|
|
2556
|
+
blocked: [],
|
|
2531
2557
|
secrets: null,
|
|
2532
2558
|
// Los inicios de sesión se piden al ENTRAR en su pestaña, no al arrancar.
|
|
2533
2559
|
logins: null,
|
package/src/tui/i18n.js
CHANGED
|
@@ -280,6 +280,14 @@ const es = {
|
|
|
280
280
|
revokeConfirm: (id) => `¿Quitar ${id} del perfil? Pierde el acceso ahora mismo. Lo que tenga guardado se le borra cuando se conecte: si está apagado, sigue ahí hasta entonces.`,
|
|
281
281
|
revoking: 'Revocando…',
|
|
282
282
|
deviceRevoked: (id) => `Revocado ${id}`,
|
|
283
|
+
// BLOQUEAR sin quitar: sigue en la cuenta, pero nadie le atiende hasta desbloquearlo.
|
|
284
|
+
deviceBlocked: 'BLOQUEADO',
|
|
285
|
+
blocking: 'Bloqueando…',
|
|
286
|
+
unblocking: 'Desbloqueando…',
|
|
287
|
+
deviceBlockedMsg: (id) => `Bloqueado ${id}: la bóveda y tus máquinas no le atienden hasta que lo desbloquees (b)`,
|
|
288
|
+
deviceUnblockedMsg: (id) => `Desbloqueado ${id}`,
|
|
289
|
+
deviceNotBlocked: (id) => `${id} no estaba bloqueado`,
|
|
290
|
+
cantBlockMaster: 'La bóveda no se bloquea a sí misma.',
|
|
283
291
|
approveLabel: (id) => `Código que MUESTRA el dispositivo ${id}`,
|
|
284
292
|
approveHint: 'el vault no lo conoce: compáralo en la otra pantalla',
|
|
285
293
|
codeMissing: 'Falta el código',
|
|
@@ -411,13 +419,13 @@ const es = {
|
|
|
411
419
|
// Anunciar teclas muertas confunde y además las quema para otros usos.
|
|
412
420
|
// `e variables` solo en un SERVICIO: es el único que las lee, así que en un teléfono
|
|
413
421
|
// era una tecla que solo sabía decir que no.
|
|
414
|
-
helpDevices: ({ pending, hasDevices, isService, drafts } = {}) => [
|
|
422
|
+
helpDevices: ({ pending, hasDevices, isService, drafts, curBlocked } = {}) => [
|
|
415
423
|
'←→ pestaña', '↑↓', 'p emparejar',
|
|
416
424
|
...(drafts ? ['g guardar permisos'] : []),
|
|
417
425
|
...(pending ? ['a aprobar', 'x rechazar'] : []),
|
|
418
426
|
...(hasDevices ? ['r renombrar', 'c permisos'] : []),
|
|
419
427
|
...(isService ? ['e variables'] : []),
|
|
420
|
-
...(hasDevices ? ['v revocar'] : []),
|
|
428
|
+
...(hasDevices ? [curBlocked ? 'b desbloquear' : 'b bloquear', 'v revocar'] : []),
|
|
421
429
|
'F5 refrescar', 'Esc bóvedas', 'l English', 'q salir'
|
|
422
430
|
],
|
|
423
431
|
renameDeviceLabel: (id) => `¿Cómo quieres llamar a ${id}?`,
|
|
@@ -718,6 +726,13 @@ const en = {
|
|
|
718
726
|
revokeConfirm: (id) => `Remove ${id} from the profile? It loses access right away. What it has stored is erased when it connects: if it is switched off, it stays there until then.`,
|
|
719
727
|
revoking: 'Revoking…',
|
|
720
728
|
deviceRevoked: (id) => `Revoked ${id}`,
|
|
729
|
+
deviceBlocked: 'BLOCKED',
|
|
730
|
+
blocking: 'Blocking…',
|
|
731
|
+
unblocking: 'Unblocking…',
|
|
732
|
+
deviceBlockedMsg: (id) => `Blocked ${id}: the vault and your machines ignore it until you unblock it (b)`,
|
|
733
|
+
deviceUnblockedMsg: (id) => `Unblocked ${id}`,
|
|
734
|
+
deviceNotBlocked: (id) => `${id} was not blocked`,
|
|
735
|
+
cantBlockMaster: 'The vault does not block itself.',
|
|
721
736
|
approveLabel: (id) => `Code SHOWN by device ${id}`,
|
|
722
737
|
approveHint: 'the vault does not know it: compare it on the other screen',
|
|
723
738
|
codeMissing: 'The code is missing',
|
|
@@ -827,13 +842,13 @@ const en = {
|
|
|
827
842
|
helpConfirm: 'y confirm · n/Esc cancel',
|
|
828
843
|
|
|
829
844
|
helpProfiles: ['↑↓', 'Enter open', 'p pair', 'j join', 'n new', 'r rename', 'd delete', 'c password', 'x drop-password', 'y keys', 'u unlock', 'k lock', 'l Español', 'q quit'],
|
|
830
|
-
helpDevices: ({ pending, hasDevices, isService, drafts } = {}) => [
|
|
845
|
+
helpDevices: ({ pending, hasDevices, isService, drafts, curBlocked } = {}) => [
|
|
831
846
|
'←→ tab', '↑↓', 'p pair',
|
|
832
847
|
...(drafts ? ['g save permissions'] : []),
|
|
833
848
|
...(pending ? ['a approve', 'x reject'] : []),
|
|
834
849
|
...(hasDevices ? ['r rename', 'c permissions'] : []),
|
|
835
850
|
...(isService ? ['e variables'] : []),
|
|
836
|
-
...(hasDevices ? ['v revoke'] : []),
|
|
851
|
+
...(hasDevices ? [curBlocked ? 'b unblock' : 'b block', 'v revoke'] : []),
|
|
837
852
|
'F5 refresh', 'Esc vaults', 'l Español', 'q quit'
|
|
838
853
|
],
|
|
839
854
|
renameDeviceLabel: (id) => `What do you want to call ${id}?`,
|
package/src/vaultControl.js
CHANGED
|
@@ -44,6 +44,9 @@ const F = {
|
|
|
44
44
|
approveReq: 'approve-request.json',
|
|
45
45
|
rejectReq: 'reject-request.json',
|
|
46
46
|
revokeReq: 'revoke-request.json',
|
|
47
|
+
// BLOQUEAR / DESBLOQUEAR un aparato (sin tocar el acta). Mismo par que usa la CLI.
|
|
48
|
+
blockReq: 'block-request.json',
|
|
49
|
+
blockRes: 'block.json',
|
|
47
50
|
labelReq: 'label-request.json',
|
|
48
51
|
capsReq: 'caps-request.json',
|
|
49
52
|
capsRes: 'caps.json',
|
|
@@ -394,6 +397,8 @@ export async function listDevices (profile) {
|
|
|
394
397
|
issued: groupCertsByDevice(withIds, revoked),
|
|
395
398
|
revoked,
|
|
396
399
|
members: record?.members || [],
|
|
400
|
+
// LOS BLOQUEADOS viajan con el acta: quien pinte la lista los marca en su fila.
|
|
401
|
+
blocked: Array.isArray(record?.blocked) ? record.blocked : [],
|
|
397
402
|
profile: devices.profile || null
|
|
398
403
|
}
|
|
399
404
|
}
|
|
@@ -502,6 +507,25 @@ export async function revokeDevice (target, profile) {
|
|
|
502
507
|
return listDevices(profile)
|
|
503
508
|
}
|
|
504
509
|
|
|
510
|
+
/**
|
|
511
|
+
* BLOQUEA o DESBLOQUEA un aparato por su llave `pub`. No toca el acta: sigue siendo
|
|
512
|
+
* miembro, pero la bóveda y los agentes dejan de atenderle (dueño, 2026-10-07). Funciona
|
|
513
|
+
* con el perfil cerrado, igual que `dotrino-vault block|unblock`. Devuelve `{ on, was }`:
|
|
514
|
+
* `was` dice si al desbloquear había algo que quitar.
|
|
515
|
+
*/
|
|
516
|
+
export async function setDeviceBlocked (pub, on, profile) {
|
|
517
|
+
requireAlive()
|
|
518
|
+
if (typeof pub !== 'string' || !pub) throw coded('a device key is required', 'BAD_PUB')
|
|
519
|
+
rm(F.blockRes)
|
|
520
|
+
const since = Date.now()
|
|
521
|
+
const id = await writeReq(F.blockReq, { pub, on: !!on }, profile)
|
|
522
|
+
signalOrCleanup('SIGUSR2', [F.blockReq])
|
|
523
|
+
const r = await waitFor(F.blockRes, { req: id, since, tries: 40 })
|
|
524
|
+
if (!r) throw coded('the daemon did not reply', 'NO_REPLY')
|
|
525
|
+
if (r.ok === false) throw coded(r.error || `could not ${on ? 'block' : 'unblock'} the device`, r.code || 'BLOCK_FAILED')
|
|
526
|
+
return { on: !!on, was: !!r.was }
|
|
527
|
+
}
|
|
528
|
+
|
|
505
529
|
// ---------------------------------------------------------------------------
|
|
506
530
|
// Perfil del usuario (lo que sincronizan los dispositivos)
|
|
507
531
|
// ---------------------------------------------------------------------------
|