@dotrino/vaultd 0.142.0 → 0.143.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.142.0",
3
+ "version": "0.143.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": {
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
- const status = d.noAccess
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
- hasDevices: (st.devices?.issued || []).length > 0,
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}?`,
@@ -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
  // ---------------------------------------------------------------------------