@dotrino/vaultd 0.15.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 +42 -19
- package/src/daemon.js +11 -10
- package/src/tui/app.js +17 -20
- package/src/tui/i18n.js +12 -14
- package/src/vault.js +9 -0
- package/src/vaultControl.js +13 -11
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
|
@@ -257,18 +257,14 @@ function cmdReject (deviceId) {
|
|
|
257
257
|
* Distinto de `members` (quién es del perfil) y de `profile` (los perfiles del PC): esto
|
|
258
258
|
* es el CONTENIDO, no la identidad.
|
|
259
259
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
260
|
+
* De la foto solo se dice que la hay, de qué tipo y cuánto pesa: es un data-URI de hasta
|
|
261
|
+
* ~90 KB y una terminal no es sitio para volcarlo.
|
|
262
262
|
*/
|
|
263
|
-
async function cmdMe (
|
|
264
|
-
const i = args.findIndex((a) => a === '--foto' || a === '--photo')
|
|
265
|
-
const avatarPath = i >= 0 ? args[i + 1] : null
|
|
266
|
-
if (i >= 0 && !avatarPath) { console.error('uso: dotrino-vault me --foto <archivo>'); process.exit(2) }
|
|
267
|
-
|
|
263
|
+
async function cmdMe () {
|
|
268
264
|
const s = requireDaemon()
|
|
269
265
|
const meFile = path.join(dir, 'me.json')
|
|
270
266
|
try { fs.rmSync(meFile, { force: true }) } catch (_) {}
|
|
271
|
-
writeReq('me-request.json', {
|
|
267
|
+
writeReq('me-request.json', {})
|
|
272
268
|
avisar(s.pid, 'SIGUSR2')
|
|
273
269
|
let dump = null
|
|
274
270
|
for (let n = 0; n < 50; n++) { await sleep(100); const d = readJson(meFile, null); if (d?.at) { dump = d; break } }
|
|
@@ -306,8 +302,6 @@ async function cmdMe (args = []) {
|
|
|
306
302
|
for (const x of lista) console.log(' %s %s%s', (x.type || x.label || '').padEnd(12), x.value, x.visible === false ? ' (oculto)' : '')
|
|
307
303
|
}
|
|
308
304
|
|
|
309
|
-
if (dump.avatarGuardada) console.log('\n Foto escrita en: %s', dump.avatarGuardada)
|
|
310
|
-
else if (me.avatar) console.log('\n Para verla: dotrino-vault me --foto ~/perfil.png')
|
|
311
305
|
console.log('')
|
|
312
306
|
}
|
|
313
307
|
|
|
@@ -348,6 +342,40 @@ async function cmdMembers () {
|
|
|
348
342
|
console.log(' Los servicios solo pueden abrir las claves de su propio nombre; eso no se cambia aquí.\n')
|
|
349
343
|
}
|
|
350
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
|
+
|
|
351
379
|
/** `dotrino-vault caps <ID> ±permiso` — cambia lo que puede hacer un dispositivo. */
|
|
352
380
|
async function cmdCaps (args = []) {
|
|
353
381
|
const [id, ...cambios] = args
|
|
@@ -360,14 +388,7 @@ async function cmdCaps (args = []) {
|
|
|
360
388
|
sign: 'sign', store: 'store', read: 'read', admin: 'admin'
|
|
361
389
|
}
|
|
362
390
|
const s = requireDaemon()
|
|
363
|
-
const
|
|
364
|
-
try { fs.rmSync(actaFile, { force: true }) } catch (_) {}
|
|
365
|
-
writeReq('dump-request.json', {})
|
|
366
|
-
avisar(s.pid, 'SIGUSR2')
|
|
367
|
-
let acta = null
|
|
368
|
-
for (let i = 0; i < 50; i++) { await sleep(100); const a = readJson(actaFile, null); if (a?.at) { acta = a; break } }
|
|
369
|
-
const m = acta?.members?.find((x) => x.id === id.toUpperCase())
|
|
370
|
-
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)
|
|
371
392
|
|
|
372
393
|
const caps = new Set(m.caps)
|
|
373
394
|
for (const c of cambios) {
|
|
@@ -648,6 +669,7 @@ function help () {
|
|
|
648
669
|
devices lista dispositivos enrolados / revocados
|
|
649
670
|
me tu perfil (nombre, foto, datos) tal como lo tiene la bóveda
|
|
650
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)
|
|
651
673
|
caps <ID> ±permiso cambia permisos (+firma -guarda +administra …)
|
|
652
674
|
revoke <nonce> revoca un dispositivo (le ordena autoborrarse)
|
|
653
675
|
activity [n] bitácora de seguridad: firmas, renovaciones, enrolados, rechazos
|
|
@@ -691,8 +713,9 @@ export async function runCtl (argv) {
|
|
|
691
713
|
case 'approve': return cmdApprove(rest[0])
|
|
692
714
|
case 'reject': return cmdReject(rest[0])
|
|
693
715
|
case 'devices': return cmdDevices()
|
|
694
|
-
case 'me': return cmdMe(
|
|
716
|
+
case 'me': return cmdMe()
|
|
695
717
|
case 'members': return cmdMembers()
|
|
718
|
+
case 'label': return cmdLabel(rest)
|
|
696
719
|
case 'caps': return cmdCaps(rest)
|
|
697
720
|
case 'revoke': return cmdRevoke(rest[0])
|
|
698
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'))
|
|
@@ -257,7 +266,7 @@ export async function runDaemon () {
|
|
|
257
266
|
// Solo se vuelca cuando se PIDE, no en cada señal: es contenido del usuario y no tiene
|
|
258
267
|
// por qué quedar escrito en un archivo suelto cada vez que alguien mira los miembros.
|
|
259
268
|
// La FOTO no entra en el volcado (son hasta ~90 KB de data-URI que nadie va a leer en
|
|
260
|
-
// una terminal): se
|
|
269
|
+
// una terminal): solo se dice que la hay, de qué tipo y cuánto pesa.
|
|
261
270
|
const meReq = readJsonSafe(meReqFile)
|
|
262
271
|
if (meReq) {
|
|
263
272
|
rm(meReqFile)
|
|
@@ -265,15 +274,7 @@ export async function runDaemon () {
|
|
|
265
274
|
const tm = resolveTarget(meReq) || { id: mgr.currentId(), vault: mgr.current() }
|
|
266
275
|
const { me } = tm.vault.threads.methods.profileGet()
|
|
267
276
|
const { avatar, ...resto } = me || {}
|
|
268
|
-
|
|
269
|
-
if (meReq.avatarPath && typeof avatar === 'string') {
|
|
270
|
-
const m = /^data:([^;,]+)?(?:;base64)?,(.*)$/s.exec(avatar)
|
|
271
|
-
if (m) {
|
|
272
|
-
fs.writeFileSync(meReq.avatarPath, Buffer.from(m[2], 'base64'), { mode: 0o600 })
|
|
273
|
-
guardada = meReq.avatarPath
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
writeJson(meFile, { v: 1, at: Date.now(), profile: tm.id, me: me ? { ...resto, avatar: avatarInfo(avatar) } : null, avatarGuardada: guardada })
|
|
277
|
+
writeJson(meFile, { v: 1, at: Date.now(), profile: tm.id, me: me ? { ...resto, avatar: avatarInfo(avatar) } : null })
|
|
277
278
|
} catch (e) { console.error('[vault] could not dump the profile:', e.message) }
|
|
278
279
|
}
|
|
279
280
|
} catch (e) {
|
package/src/tui/app.js
CHANGED
|
@@ -268,7 +268,6 @@ function meRows (st, t) {
|
|
|
268
268
|
rows.push({ text: t.accent(' ▸ ' + titulo), sel: false })
|
|
269
269
|
for (const x of lista) campo(x.type || x.label || '', x.value, x.visible === false)
|
|
270
270
|
}
|
|
271
|
-
if (me.avatar) { rows.push({ text: '', sel: false }); rows.push({ text: t.muted(i.savePhotoHint), sel: false }) }
|
|
272
271
|
return rows
|
|
273
272
|
}
|
|
274
273
|
|
|
@@ -499,6 +498,20 @@ async function onKeyDevices (term, st, key) {
|
|
|
499
498
|
},
|
|
500
499
|
onNo: () => { st.confirm = null }
|
|
501
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
|
+
})
|
|
502
515
|
} else if (ch === 'r') {
|
|
503
516
|
await refreshDevices(term, st)
|
|
504
517
|
}
|
|
@@ -603,27 +616,11 @@ async function onKeyPairing (term, st, key) {
|
|
|
603
616
|
}
|
|
604
617
|
|
|
605
618
|
/**
|
|
606
|
-
* Teclas del PERFIL: refrescar
|
|
607
|
-
*
|
|
619
|
+
* Teclas del PERFIL: solo refrescar. Nada de editar — el perfil se edita en el dispositivo
|
|
620
|
+
* que usas, no en la máquina donde vive la bóveda.
|
|
608
621
|
*/
|
|
609
622
|
async function onKeyMe (term, st, key) {
|
|
610
|
-
|
|
611
|
-
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
612
|
-
if (ch === 'r') {
|
|
613
|
-
await refreshMe(term, st)
|
|
614
|
-
} else if (ch === 'f' && st.me?.avatar) {
|
|
615
|
-
setInput(st, {
|
|
616
|
-
label: i.savePhotoLabel,
|
|
617
|
-
hint: i.savePhotoHintInput,
|
|
618
|
-
value: 'perfil.png',
|
|
619
|
-
onSubmit: async (valor) => {
|
|
620
|
-
const destino = String(valor || '').trim()
|
|
621
|
-
if (!destino) return
|
|
622
|
-
const r = await guard(term, st, i.savingPhoto, () => vc.saveAvatar(destino, activeId(st)))
|
|
623
|
-
if (r.ok && r.v) flash(st, i.photoSaved(r.v))
|
|
624
|
-
}
|
|
625
|
-
})
|
|
626
|
-
}
|
|
623
|
+
if (key.name === 'char' && key.ch.toLowerCase() === 'r') await refreshMe(term, st)
|
|
627
624
|
return true
|
|
628
625
|
}
|
|
629
626
|
|
package/src/tui/i18n.js
CHANGED
|
@@ -53,12 +53,7 @@ const es = {
|
|
|
53
53
|
hidden: ' (oculto)',
|
|
54
54
|
noName: '(sin nombre)',
|
|
55
55
|
no: 'no',
|
|
56
|
-
|
|
57
|
-
savePhotoLabel: '¿Dónde guardo la foto?',
|
|
58
|
-
savePhotoHintInput: 'ruta del archivo (Esc cancela)',
|
|
59
|
-
savingPhoto: 'Guardando la foto…',
|
|
60
|
-
photoSaved: (ruta) => `Foto guardada en ${ruta}`,
|
|
61
|
-
helpMe: ['←→ pestaña', 'r refrescar', 'f guardar foto', 'Esc bóvedas', 'l English', 'q salir'],
|
|
56
|
+
helpMe: ['←→ pestaña', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
|
|
62
57
|
tabsHint: ' (←→ cambiar)',
|
|
63
58
|
titleProfiles: 'Bóvedas',
|
|
64
59
|
titlePairing: 'Emparejar un dispositivo',
|
|
@@ -180,7 +175,11 @@ const es = {
|
|
|
180
175
|
// language/quit); lo único que se traduce es la palabra que las explica.
|
|
181
176
|
// Segmentos, no una línea: el render recorta del medio si no caben.
|
|
182
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'],
|
|
183
|
-
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}»`,
|
|
184
183
|
helpSecrets: ['←→ pestaña', '↑↓', 'n nueva variable', 'x quitar (variable/scope)', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
|
|
185
184
|
helpPairing: ['a aprobar', 'x rechazar', 'r reiniciar', '↑↓ scroll', 'Esc atrás', 'l English'],
|
|
186
185
|
helpPairMode: ['↑↓', 'Enter elegir', 'Esc atrás', 'l English', 'q salir'],
|
|
@@ -245,12 +244,7 @@ const en = {
|
|
|
245
244
|
hidden: ' (hidden)',
|
|
246
245
|
noName: '(no name)',
|
|
247
246
|
no: 'no',
|
|
248
|
-
|
|
249
|
-
savePhotoLabel: 'Where should I save the photo?',
|
|
250
|
-
savePhotoHintInput: 'file path (Esc cancels)',
|
|
251
|
-
savingPhoto: 'Saving the photo…',
|
|
252
|
-
photoSaved: (ruta) => `Photo saved to ${ruta}`,
|
|
253
|
-
helpMe: ['←→ tab', 'r refresh', 'f save photo', 'Esc vaults', 'l Español', 'q quit'],
|
|
247
|
+
helpMe: ['←→ tab', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
|
|
254
248
|
tabsHint: ' (←→ switch)',
|
|
255
249
|
titleProfiles: 'Vaults',
|
|
256
250
|
titlePairing: 'Pair a device',
|
|
@@ -361,7 +355,11 @@ const en = {
|
|
|
361
355
|
helpConfirm: 'y confirm · n/Esc cancel',
|
|
362
356
|
|
|
363
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'],
|
|
364
|
-
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}"`,
|
|
365
363
|
helpSecrets: ['←→ tab', '↑↓', 'n new variable', 'x remove (variable/scope)', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
|
|
366
364
|
helpPairing: ['a approve', 'x reject', 'r restart', '↑↓ scroll', 'Esc back', 'l Español'],
|
|
367
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()
|
|
@@ -234,17 +247,6 @@ export async function getMe (profile) {
|
|
|
234
247
|
return d.me || null
|
|
235
248
|
}
|
|
236
249
|
|
|
237
|
-
/** Escribe la foto de perfil en `destino` (para poder mirarla) y devuelve la ruta. */
|
|
238
|
-
export async function saveAvatar (destino, profile) {
|
|
239
|
-
requireAlive()
|
|
240
|
-
rm(F.me)
|
|
241
|
-
writeReq(F.meReq, { avatarPath: destino }, profile)
|
|
242
|
-
signalOrCleanup('SIGUSR2', [F.meReq])
|
|
243
|
-
const d = await waitFor(F.me)
|
|
244
|
-
rm(F.me)
|
|
245
|
-
if (!d) throw coded('the daemon did not reply', 'NO_REPLY')
|
|
246
|
-
return d.avatarGuardada || null
|
|
247
|
-
}
|
|
248
250
|
|
|
249
251
|
// ---------------------------------------------------------------------------
|
|
250
252
|
// Secretos: scopes (namespaces) y variables (claves)
|