@dotrino/vaultd 0.22.0 → 0.24.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/lib/src/admin.js +8 -0
- package/lib/src/enroll.js +24 -1
- package/lib/src/index.js +4 -2
- package/package.json +2 -2
- package/src/ctl.js +39 -8
- package/src/daemon.js +7 -3
- package/src/tui/app.js +27 -4
- package/src/vault.js +45 -4
- package/src/vaultControl.js +35 -4
package/lib/src/admin.js
CHANGED
|
@@ -127,7 +127,15 @@ export function createAdminDesk ({
|
|
|
127
127
|
return { ok: true, result: { ok: true } }
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// Quitar un dispositivo: por `sub` se le retiran TODOS sus certificados. Con
|
|
131
|
+
// `certNonce` a secas solo cae ese papel, y el aparato puede tener otro vigente.
|
|
130
132
|
if (data.op === 'revoke') {
|
|
133
|
+
if (data.sub) {
|
|
134
|
+
const r = await desk.revokeDevice(String(data.sub))
|
|
135
|
+
audit('admin.revoke-device', { by, certs: r?.nonces?.length ?? null })
|
|
136
|
+
await notify('revoked', { by })
|
|
137
|
+
return { ok: true, result: r || { ok: true } }
|
|
138
|
+
}
|
|
131
139
|
const r = await desk.revoke(String(data.certNonce || ''))
|
|
132
140
|
audit('admin.revoke', { by, certNonce: data.certNonce })
|
|
133
141
|
await notify('revoked', { certNonce: data.certNonce, by })
|
package/lib/src/enroll.js
CHANGED
|
@@ -441,9 +441,32 @@ export function createEnrollDesk ({
|
|
|
441
441
|
return res
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
+
/**
|
|
445
|
+
* QUITA EL DISPOSITIVO entero: retira TODOS sus certificados vigentes y le manda una
|
|
446
|
+
* sola orden de autoborrado. `revoke(nonce)` retira un papel, y un aparato puede tener
|
|
447
|
+
* varios (una renovación dejaba vivo el anterior): quitarle uno no lo echaba, y podía
|
|
448
|
+
* quedarse dentro justo con el que llevaba `vault:admin`.
|
|
449
|
+
*/
|
|
450
|
+
async function revokeDevice (sub) {
|
|
451
|
+
if (!sub) throw new Error('sub (device pubkey) required')
|
|
452
|
+
const { issued } = await identity.listDelegations()
|
|
453
|
+
const mine = (issued || []).filter((d) => d.sub === sub)
|
|
454
|
+
audit('revoke-device', { certs: mine.length })
|
|
455
|
+
// Si el núcleo no trae `revokeDevice` (bóveda vieja), se cae a retirarlos uno a uno.
|
|
456
|
+
const res = identity.revokeDevice
|
|
457
|
+
? await identity.revokeDevice(sub)
|
|
458
|
+
: { ok: true, nonces: await (async () => {
|
|
459
|
+
const done = []
|
|
460
|
+
for (const d of mine) { await identity.revokeDelegation(d.nonce); done.push(d.nonce) }
|
|
461
|
+
return done
|
|
462
|
+
})() }
|
|
463
|
+
await emitRevoke(sub, mine[0]?.nonce || null)
|
|
464
|
+
return res
|
|
465
|
+
}
|
|
466
|
+
|
|
444
467
|
return {
|
|
445
468
|
startPairing, stopPairing, handleEnroll, handleActaSealed, handleHello, approve, reject,
|
|
446
|
-
listPending, findPending, emitRevoke, revoke,
|
|
469
|
+
listPending, findPending, emitRevoke, revoke, revokeDevice,
|
|
447
470
|
get pendingCount () { return pending.size }
|
|
448
471
|
}
|
|
449
472
|
}
|
package/lib/src/index.js
CHANGED
|
@@ -155,14 +155,16 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
155
155
|
if (typeof d.ts !== 'number' || Math.abs(Date.now() - d.ts) > FRESH_WINDOW_MS) return
|
|
156
156
|
const chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, trustedIssuer: iss })
|
|
157
157
|
if (!chk.ok) return send(from, { type: MSG.ERROR, error: 'no autorizado: ' + chk.reason })
|
|
158
|
-
const { issued, revoked } = await identity.listDelegations()
|
|
158
|
+
const { issued, revoked, revokedCerts } = await identity.listDelegations()
|
|
159
159
|
const devices = await Promise.all((issued || []).map(async (x) => ({
|
|
160
160
|
deviceId: x.sub ? await deviceIdOf(x.sub) : null, sub: x.sub || null,
|
|
161
161
|
label: x.label || '', scope: x.scope, exp: x.exp, nonce: x.nonce
|
|
162
162
|
})))
|
|
163
163
|
send(from, { type: MSG.DEVICES_RESULT, devices, revoked: (revoked || []).map((r) => r.nonce || r) })
|
|
164
164
|
// ¿el que consulta es una máquina revocada que reapareció? → re-emite el REVOKED firmado.
|
|
165
|
-
|
|
165
|
+
// Se busca en `revokedCerts`: desde que `issued` solo trae lo vigente, los retirados
|
|
166
|
+
// ya no están ahí y este aviso dejaba de dispararse (se caía en silencio).
|
|
167
|
+
const mine = (revokedCerts || issued || []).find((x) => x.sub === chk.device && x.revokedAt)
|
|
166
168
|
if (mine) desk.emitRevoke(chk.device, mine.nonce)
|
|
167
169
|
}
|
|
168
170
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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.44.0",
|
|
23
23
|
"@dotrino/proxy-client": "^0.10.0",
|
|
24
24
|
"ws": "^8.18.0"
|
|
25
25
|
},
|
package/src/ctl.js
CHANGED
|
@@ -410,13 +410,27 @@ async function cmdDevices () {
|
|
|
410
410
|
let snap = null
|
|
411
411
|
for (let i = 0; i < 50; i++) { await sleep(100); const d = readJson(devFile, null); if (d?.at) { snap = d; break } }
|
|
412
412
|
if (!snap) { console.error('El daemon no respondió.'); process.exit(1) }
|
|
413
|
-
const active = snap.issued || snap.active || snap.delegations || []
|
|
414
413
|
const revoked = snap.revoked || []
|
|
414
|
+
const fuera = new Set(revoked.map((r) => r?.nonce || r))
|
|
415
|
+
// UN APARATO, UNA LÍNEA. El daemon lleva la cuenta por CERTIFICADO —correcto para él,
|
|
416
|
+
// porque revocar es revocar un papel—, pero renovar emite uno nuevo cada 30 días: un
|
|
417
|
+
// aparato de un año salía doce veces, y los ya retirados seguían contando como
|
|
418
|
+
// enrolados. Se agrupa por llave y se dice cuántos certificados tiene.
|
|
419
|
+
const porLlave = new Map()
|
|
420
|
+
for (const d of (snap.issued || snap.active || snap.delegations || [])) {
|
|
421
|
+
if (d.revokedAt || fuera.has(d.nonce)) continue
|
|
422
|
+
const clave = d.sub || d.nonce
|
|
423
|
+
const y = porLlave.get(clave)
|
|
424
|
+
if (!y) porLlave.set(clave, { ...d, certs: 1 })
|
|
425
|
+
else { y.certs++; if ((d.exp || 0) > (y.exp || 0)) Object.assign(y, { ...d, certs: y.certs }) }
|
|
426
|
+
}
|
|
427
|
+
const active = [...porLlave.values()]
|
|
415
428
|
console.log('Dispositivos enrolados: %d', active.length)
|
|
416
429
|
for (const d of active) {
|
|
417
430
|
const did = d.sub ? await deviceIdOf(d.sub) : '????-????'
|
|
418
|
-
console.log(' · %s %s%s%s', did, d.label || '(sin etiqueta)',
|
|
419
|
-
d.exp ? '
|
|
431
|
+
console.log(' · %s %s%s%s%s', did, d.label || '(sin etiqueta)',
|
|
432
|
+
d.exp ? ' vence=' + new Date(d.exp).toISOString().slice(0, 10) : '',
|
|
433
|
+
d.certs > 1 ? ' (' + d.certs + ' certificados)' : '',
|
|
420
434
|
d.nonce ? ' nonce=' + d.nonce : '')
|
|
421
435
|
}
|
|
422
436
|
if (revoked.length) {
|
|
@@ -426,12 +440,29 @@ async function cmdDevices () {
|
|
|
426
440
|
console.log('\nPara revocar uno (y ordenarle autoborrarse): dotrino-vault revoke <nonce>')
|
|
427
441
|
}
|
|
428
442
|
|
|
429
|
-
|
|
430
|
-
|
|
443
|
+
/**
|
|
444
|
+
* `dotrino-vault revoke <ID|nonce>` — quita un dispositivo.
|
|
445
|
+
*
|
|
446
|
+
* Con el IDENTIFICADOR del aparato (`AB12-CD34`) se le retiran TODOS sus certificados,
|
|
447
|
+
* que es lo que la gente quiere decir con «quitar este dispositivo»: renovar emite uno
|
|
448
|
+
* nuevo cada 30 días, así que quitar solo el último dejaba vivos los anteriores hasta que
|
|
449
|
+
* caducaran — quitarlo sin quitarlo. Con un `nonce` suelto se retira ese y solo ese, que
|
|
450
|
+
* sigue siendo útil para casos finos.
|
|
451
|
+
*/
|
|
452
|
+
async function cmdRevoke (arg) {
|
|
453
|
+
if (!arg) { console.error('uso: dotrino-vault revoke <ID|nonce> (el ID quita el aparato entero)'); process.exit(2) }
|
|
454
|
+
const esId = /^[0-9a-f]{4}-?[0-9a-f]{4}$/i.test(arg)
|
|
431
455
|
const s = requireDaemon()
|
|
432
|
-
|
|
456
|
+
if (esId) {
|
|
457
|
+
const m = await buscarMiembro(arg.toUpperCase().includes('-') ? arg.toUpperCase() : arg.toUpperCase().replace(/(.{4})(.{4})/, '$1-$2'))
|
|
458
|
+
writeReq('revoke-request.json', { sub: m.pub })
|
|
459
|
+
avisar(s.pid, 'SIGUSR2')
|
|
460
|
+
console.log('Quitado %s (todos sus certificados). Se autoborrará al reconectar. Verifica: dotrino-vault devices', m.id)
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
writeReq('revoke-request.json', { nonce: arg })
|
|
433
464
|
avisar(s.pid, 'SIGUSR2')
|
|
434
|
-
console.log('Revocación enviada para nonce=%s. El dispositivo se autoborrará al reconectar. Verifica: dotrino-vault devices',
|
|
465
|
+
console.log('Revocación enviada para nonce=%s. El dispositivo se autoborrará al reconectar. Verifica: dotrino-vault devices', arg)
|
|
435
466
|
}
|
|
436
467
|
|
|
437
468
|
/**
|
|
@@ -671,7 +702,7 @@ function help () {
|
|
|
671
702
|
members el acta del perfil: quién es tuyo y qué puede hacer
|
|
672
703
|
label <ID> <nombre> renombra un dispositivo (el nombre con el que lo reconoces)
|
|
673
704
|
caps <ID> ±permiso cambia permisos (+firma -guarda +administra …)
|
|
674
|
-
revoke <nonce>
|
|
705
|
+
revoke <ID|nonce> quita un dispositivo (con el ID, todos sus certificados)
|
|
675
706
|
activity [n] bitácora de seguridad: firmas, renovaciones, enrolados, rechazos
|
|
676
707
|
logs últimos logs del servicio
|
|
677
708
|
version muestra la versión instalada
|
package/src/daemon.js
CHANGED
|
@@ -222,10 +222,14 @@ export async function runDaemon () {
|
|
|
222
222
|
console.log('[vault] permissions updated: %s', capsReq.caps.join(', ') || '(ninguno)')
|
|
223
223
|
} catch (e) { console.error('[vault] could not change permissions:', e.message) }
|
|
224
224
|
}
|
|
225
|
+
// Quitar un dispositivo: se pide por `sub` (la llave del aparato). `nonce` sigue
|
|
226
|
+
// aceptado para una consola vieja, pero retira UN certificado, no el aparato.
|
|
225
227
|
const req = readJsonSafe(revokeReqFile)
|
|
226
|
-
if (req?.nonce) {
|
|
227
|
-
try {
|
|
228
|
-
|
|
228
|
+
if (req?.sub || req?.nonce) {
|
|
229
|
+
try {
|
|
230
|
+
await targetOf(req)?.revokeDevice(req.sub ? { sub: req.sub } : { nonce: req.nonce })
|
|
231
|
+
console.log(req.sub ? '[vault] device removed' : '[vault] revoked nonce=%s', req.nonce || '')
|
|
232
|
+
} catch (e) { console.error('[vault] revocation failed:', e.message) }
|
|
229
233
|
rm(revokeReqFile)
|
|
230
234
|
}
|
|
231
235
|
// Secretos de servicios: `secret set/rm` del CLI. El archivo con el valor
|
package/src/tui/app.js
CHANGED
|
@@ -70,6 +70,24 @@ const shortScope = (scope) => {
|
|
|
70
70
|
return arr.map((s) => String(s).replace(/^vault:/, '')).join(',') || '—'
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Un aparato = una llave (`sub`), aunque tenga varios certificados vigentes. Se queda con
|
|
75
|
+
* el de vencimiento más lejano (el último emitido) y suma los alcances de todos, para que
|
|
76
|
+
* la fila no prometa menos de lo que el aparato puede hacer de verdad.
|
|
77
|
+
*/
|
|
78
|
+
function groupByDevice (issued) {
|
|
79
|
+
const by = new Map()
|
|
80
|
+
for (const d of issued) {
|
|
81
|
+
const key = d.sub || d.deviceId || d.nonce
|
|
82
|
+
const prev = by.get(key)
|
|
83
|
+
if (!prev) { by.set(key, { ...d, certCount: 1, scope: [...(Array.isArray(d.scope) ? d.scope : [d.scope])] }); continue }
|
|
84
|
+
prev.certCount++
|
|
85
|
+
for (const s of (Array.isArray(d.scope) ? d.scope : [d.scope])) if (!prev.scope.includes(s)) prev.scope.push(s)
|
|
86
|
+
if ((d.exp || 0) > (prev.exp || 0)) { prev.exp = d.exp; prev.nonce = d.nonce; prev.label = d.label || prev.label }
|
|
87
|
+
}
|
|
88
|
+
return [...by.values()]
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
function activeProfile (st) {
|
|
74
92
|
const list = st.profiles?.profiles || []
|
|
75
93
|
return list.find((p) => p.current) || list[0] || null
|
|
@@ -172,13 +190,17 @@ function deviceRows (st, t) {
|
|
|
172
190
|
rows.push({ text: t.warn(i.pendingDevice(pend.deviceId)) + t.muted(i.pendingHint), sel: false })
|
|
173
191
|
rows.push({ text: '', sel: false })
|
|
174
192
|
}
|
|
175
|
-
|
|
193
|
+
// UNA FILA POR APARATO, no por certificado. Antes se pintaba `issued` tal cual y un
|
|
194
|
+
// aparato con dos certs (el viejo + el de la renovación) salía dos veces: parecían dos
|
|
195
|
+
// máquinas. Se agrupa por llave y se muestra el cert vigente más largo.
|
|
196
|
+
const issued = groupByDevice(st.devices?.issued || [])
|
|
176
197
|
if (!issued.length) {
|
|
177
198
|
rows.push({ text: t.muted(i.noDevices), sel: false })
|
|
178
199
|
}
|
|
179
200
|
for (const d of issued) {
|
|
180
201
|
const label = d.label || t.muted(i.noLabel)
|
|
181
|
-
const
|
|
202
|
+
const extra = d.certCount > 1 ? t.muted(` certs:${d.certCount}`) : ''
|
|
203
|
+
const line = ` ${t.bold(d.deviceId)} ${label} ${t.muted('scope:' + shortScope(d.scope))} ${t.muted('exp:' + fmtExp(d.exp))}${extra}`
|
|
182
204
|
rows.push({ text: line, sel: true, meta: d })
|
|
183
205
|
}
|
|
184
206
|
const revoked = st.devices?.revoked || []
|
|
@@ -523,12 +545,13 @@ async function onKeyDevices (term, st, key) {
|
|
|
523
545
|
if (!st.pending) { flash(st, i.noPendingToReject, 'warn'); return true }
|
|
524
546
|
const r = await guard(term, st, i.rejecting, () => vc.rejectPending(st.pending.deviceId, activeId(st)))
|
|
525
547
|
if (r.ok) { flash(st, i.deviceRejected); st.pending = null }
|
|
526
|
-
} else if ((ch === 'v' || key.name === 'delete') && cur?.nonce != null) { //
|
|
548
|
+
} else if ((ch === 'v' || key.name === 'delete') && (cur?.sub || cur?.nonce != null)) { // quitar el aparato seleccionado
|
|
527
549
|
setConfirm(st, {
|
|
528
550
|
text: i.revokeConfirm(cur.deviceId),
|
|
529
551
|
onYes: async () => {
|
|
530
552
|
st.confirm = null
|
|
531
|
-
|
|
553
|
+
// Por `sub`: se le retiran TODOS los certificados, no solo el de esta fila.
|
|
554
|
+
const r = await guard(term, st, i.revoking, () => vc.revokeDevice({ sub: cur.sub, nonce: cur.nonce }, activeId(st)))
|
|
532
555
|
if (r.ok) { flash(st, i.deviceRevoked(cur.deviceId)); st.devices = r.v; st.sel.devices = 0 }
|
|
533
556
|
},
|
|
534
557
|
onNo: () => { st.confirm = null }
|
package/src/vault.js
CHANGED
|
@@ -217,10 +217,40 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
217
217
|
|
|
218
218
|
// Lista (solo lectura) de dispositivos enrolados, para un panel en el navegador.
|
|
219
219
|
// Cualquier cert válido tuyo puede verla; REVOCAR sigue siendo solo desde el PC.
|
|
220
|
+
/**
|
|
221
|
+
* Un aparato REVOCADO que vuelve a aparecer: se le reemite el `vault.revoked` FIRMADO
|
|
222
|
+
* para que se entere y se auto-borre.
|
|
223
|
+
*
|
|
224
|
+
* Un «unauthorized: revoked» suelto no basta y no debe bastar: no va firmado, así que
|
|
225
|
+
* el dispositivo tiene prohibido borrar nada con él (si no, cualquiera destruiría datos
|
|
226
|
+
* ajenos con un mensaje). Lo único que puede borrar es un aviso firmado por la maestra
|
|
227
|
+
* — y hasta ahora el daemon no lo reemitía nunca. Si el aparato estaba apagado cuando lo
|
|
228
|
+
* quitaste, no se enteraba jamás: seguía enseñando el perfil como si nada.
|
|
229
|
+
*/
|
|
230
|
+
async function avisarSiRevocado (pubkey) {
|
|
231
|
+
if (typeof pubkey !== 'string') return
|
|
232
|
+
try {
|
|
233
|
+
// OJO con dónde se buscan: desde identity 0.42 `issued` es «lo que HOY sirve para
|
|
234
|
+
// entrar», así que los retirados NO están ahí — viven en `revokedCerts`. Buscarlos
|
|
235
|
+
// en `issued` no daba error, daba una lista vacía y ningún aviso.
|
|
236
|
+
const dele = await identity.listDelegations()
|
|
237
|
+
const revocados = await revocationSet()
|
|
238
|
+
const candidatos = [...(dele.revokedCerts || []), ...(dele.issued || [])]
|
|
239
|
+
const suyas = candidatos.filter((x) => x.sub === pubkey && (x.revokedAt || revocados.has(x.nonce)))
|
|
240
|
+
if (suyas.length) {
|
|
241
|
+
await desk.emitRevoke(pubkey, suyas[0].nonce)
|
|
242
|
+
audit('revoke.notified', { device: await deviceIdOf(pubkey).catch(() => null) })
|
|
243
|
+
}
|
|
244
|
+
} catch (e) { log('[vault] could not re-emit the revocation:', e.message) }
|
|
245
|
+
}
|
|
246
|
+
|
|
220
247
|
async function handleDevices (from, p) {
|
|
221
248
|
if (!isFresh(p.data)) return staleReply(from)
|
|
222
249
|
const chk = await verifyChain({ data: p.data, signature: p.signature, cert: p.cert, trustedIssuer: master, revoked: await revocationSet() })
|
|
223
|
-
if (!chk.ok)
|
|
250
|
+
if (!chk.ok) {
|
|
251
|
+
if (chk.reason === 'revoked') await avisarSiRevocado(p.data?.publickey)
|
|
252
|
+
return reply(from, { type: MSG.ERROR, error: 'unauthorized: ' + chk.reason })
|
|
253
|
+
}
|
|
224
254
|
const { issued, revoked } = await identity.listDelegations()
|
|
225
255
|
// El acta viaja con la lista: así cada dispositivo se entera de los cambios de
|
|
226
256
|
// política (quién manda, quién puede qué) sin un canal aparte.
|
|
@@ -534,9 +564,20 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
534
564
|
await notifyMembers('label', { deviceId: device, label: r?.label ?? label })
|
|
535
565
|
return r
|
|
536
566
|
},
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
567
|
+
// QUITAR EL DISPOSITIVO. Se identifica por su llave (`sub`), no por un `nonce`: un
|
|
568
|
+
// aparato puede tener varios certificados y retirar uno no lo echaba de la bóveda.
|
|
569
|
+
// Se sigue aceptando `{ nonce }` para no romper una consola vieja en vuelo.
|
|
570
|
+
revokeDevice: async (target) => {
|
|
571
|
+
const sub = typeof target === 'object' && target ? target.sub : null
|
|
572
|
+
if (!sub) {
|
|
573
|
+
const nonce = typeof target === 'string' ? target : target?.nonce
|
|
574
|
+
const r = await desk.revoke(nonce)
|
|
575
|
+
await notifyMembers('revoked', { certNonce: nonce, by: 'pc' })
|
|
576
|
+
return r
|
|
577
|
+
}
|
|
578
|
+
const r = await desk.revokeDevice(sub)
|
|
579
|
+
audit('revoke-device', { device: await deviceIdOf(sub).catch(() => null), certs: r?.nonces?.length ?? null })
|
|
580
|
+
await notifyMembers('revoked', { deviceId: await deviceIdOf(sub).catch(() => null), by: 'pc' })
|
|
540
581
|
return r
|
|
541
582
|
},
|
|
542
583
|
close () {
|
package/src/vaultControl.js
CHANGED
|
@@ -196,10 +196,36 @@ export async function listDevices (profile) {
|
|
|
196
196
|
const { devices } = await snapshot(profile)
|
|
197
197
|
if (!devices) throw coded('the daemon did not reply', 'NO_REPLY')
|
|
198
198
|
const issued = devices.issued || devices.active || devices.delegations || []
|
|
199
|
+
const revoked = devices.revoked || []
|
|
199
200
|
const withIds = await Promise.all(issued.map(async (d) => ({
|
|
200
201
|
...d, deviceId: d.sub ? await deviceIdOf(d.sub) : '????-????'
|
|
201
202
|
})))
|
|
202
|
-
return { issued: withIds, revoked
|
|
203
|
+
return { issued: agruparPorAparato(withIds, revoked), revoked, profile: devices.profile || null }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* UN APARATO, UNA FILA — y sin los certificados retirados.
|
|
208
|
+
*
|
|
209
|
+
* El daemon lleva la cuenta por CERTIFICADO, que es lo correcto para él: revocar es
|
|
210
|
+
* revocar un papel. Pero al dueño le sobra ese detalle — renovar emite uno nuevo cada 30
|
|
211
|
+
* días, así que un aparato de un año saldría doce veces, y los revocados seguían contando
|
|
212
|
+
* como «enrolados». Se agrupa por llave, se queda el más nuevo y se guardan TODOS sus
|
|
213
|
+
* nonces, que es lo que hace falta para retirarlo entero.
|
|
214
|
+
*/
|
|
215
|
+
export function agruparPorAparato (lista, revoked = []) {
|
|
216
|
+
const fuera = new Set(revoked.map((r) => r?.nonce || r))
|
|
217
|
+
const porLlave = new Map()
|
|
218
|
+
for (const d of lista) {
|
|
219
|
+
if (d.revokedAt || fuera.has(d.nonce)) continue
|
|
220
|
+
const clave = d.sub || d.nonce
|
|
221
|
+
const y = porLlave.get(clave)
|
|
222
|
+
if (!y) porLlave.set(clave, { ...d, nonces: [d.nonce] })
|
|
223
|
+
else {
|
|
224
|
+
y.nonces.push(d.nonce)
|
|
225
|
+
if ((d.exp || 0) > (y.exp || 0)) Object.assign(y, { ...d, nonces: y.nonces })
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return [...porLlave.values()]
|
|
203
229
|
}
|
|
204
230
|
|
|
205
231
|
/**
|
|
@@ -245,10 +271,15 @@ export async function setDeviceCaps (pub, caps, profile) {
|
|
|
245
271
|
return listDevices(profile)
|
|
246
272
|
}
|
|
247
273
|
|
|
248
|
-
/**
|
|
249
|
-
|
|
274
|
+
/**
|
|
275
|
+
* Quita un dispositivo por su llave `sub` (le ordena autoborrarse) y revuelca.
|
|
276
|
+
* Se acepta un `nonce` suelto por compatibilidad, pero eso retira UN certificado:
|
|
277
|
+
* un aparato puede tener varios y seguiría entrando con el otro.
|
|
278
|
+
*/
|
|
279
|
+
export async function revokeDevice (target, profile) {
|
|
250
280
|
requireAlive()
|
|
251
|
-
|
|
281
|
+
const req = typeof target === 'string' ? { nonce: target } : { sub: target?.sub, nonce: target?.nonce }
|
|
282
|
+
writeReq(F.revokeReq, req, profile)
|
|
252
283
|
signalOrCleanup('SIGUSR2', [F.revokeReq])
|
|
253
284
|
await sleep(300)
|
|
254
285
|
return listDevices(profile)
|