@dotrino/vaultd 0.17.0 → 0.19.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 +1 -1
- package/src/tui/app.js +115 -16
- package/src/tui/i18n.js +74 -8
- package/src/tui/term.js +10 -2
- package/src/vaultControl.js +33 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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
|
@@ -213,6 +213,37 @@ function pairModeRows (st, t) {
|
|
|
213
213
|
return rows
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
/**
|
|
217
|
+
* PERMISOS de un dispositivo. Los cuatro que existen, con lo que significan en cristiano y
|
|
218
|
+
* una marca de si los tiene. El de administrar va aparte y avisado: es el único que deja
|
|
219
|
+
* a ese aparato meter y sacar dispositivos sin venir aquí.
|
|
220
|
+
*/
|
|
221
|
+
const CAPS_ORDEN = ['sign', 'store', 'read', 'admin']
|
|
222
|
+
|
|
223
|
+
function capsRows (st, t) {
|
|
224
|
+
const i = L(st)
|
|
225
|
+
const objetivo = st.capsFor
|
|
226
|
+
if (!objetivo) return [{ text: t.muted(i.loading), sel: false }]
|
|
227
|
+
const miembro = (st.members || []).find((m) => m.pub === objetivo.pub)
|
|
228
|
+
if (!miembro) return [{ text: t.muted(i.capsNoMember), sel: false }]
|
|
229
|
+
|
|
230
|
+
const tiene = new Set(miembro.caps || [])
|
|
231
|
+
const rows = [
|
|
232
|
+
{ text: ' ' + t.bold(i.capsFor(objetivo.deviceId, miembro.label || '')), sel: false },
|
|
233
|
+
{ text: '', sel: false }
|
|
234
|
+
]
|
|
235
|
+
for (const cap of CAPS_ORDEN) {
|
|
236
|
+
const marca = tiene.has(cap) ? '[x]' : '[ ]'
|
|
237
|
+
const nombre = i.capName[cap]
|
|
238
|
+
const linea = ` ${marca} ${cap === 'admin' ? t.bold(nombre) : nombre}`
|
|
239
|
+
rows.push({ text: linea, sel: true, meta: { cap } })
|
|
240
|
+
rows.push({ text: t.muted(' ' + i.capHint[cap]), sel: false })
|
|
241
|
+
}
|
|
242
|
+
rows.push({ text: '', sel: false })
|
|
243
|
+
rows.push({ text: t.muted(' ' + i.capsApplyHint), sel: false })
|
|
244
|
+
return rows
|
|
245
|
+
}
|
|
246
|
+
|
|
216
247
|
function secretRows (st, t) {
|
|
217
248
|
const i = L(st)
|
|
218
249
|
const ns = st.secrets || {}
|
|
@@ -306,6 +337,10 @@ async function refreshSecrets (term, st) {
|
|
|
306
337
|
const r = await guard(term, st, L(st).loadingSecrets, () => vc.listSecrets(activeId(st)))
|
|
307
338
|
if (r.ok) st.secrets = r.v
|
|
308
339
|
}
|
|
340
|
+
async function refreshMembers (term, st) {
|
|
341
|
+
const r = await guard(term, st, L(st).loadingMembers, () => vc.listMembers(activeId(st)))
|
|
342
|
+
if (r.ok) st.members = r.v
|
|
343
|
+
}
|
|
309
344
|
async function refreshMe (term, st) {
|
|
310
345
|
const r = await guard(term, st, L(st).loadingProfile, () => vc.getMe(activeId(st)))
|
|
311
346
|
st.me = r.ok ? r.v : null
|
|
@@ -498,9 +533,10 @@ async function onKeyDevices (term, st, key) {
|
|
|
498
533
|
},
|
|
499
534
|
onNo: () => { st.confirm = null }
|
|
500
535
|
})
|
|
501
|
-
} else if (ch === '
|
|
536
|
+
} else if (ch === 'r' && cur) {
|
|
502
537
|
// 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.
|
|
538
|
+
// con TU apodo de ese momento), así que a la semana ya no dice nada. `r` es renombrar
|
|
539
|
+
// también en Bóvedas: una tecla, un significado.
|
|
504
540
|
setInput(st, {
|
|
505
541
|
label: i.renameDeviceLabel(cur.deviceId),
|
|
506
542
|
hint: i.renameDeviceHint,
|
|
@@ -512,7 +548,12 @@ async function onKeyDevices (term, st, key) {
|
|
|
512
548
|
if (r.ok) { st.devices = r.v; flash(st, i.deviceRenamed(nombre)) }
|
|
513
549
|
}
|
|
514
550
|
})
|
|
515
|
-
} else if (ch === '
|
|
551
|
+
} else if (ch === 'c' && cur?.sub) {
|
|
552
|
+
st.capsFor = { pub: cur.sub, deviceId: cur.deviceId }
|
|
553
|
+
st.sel.caps = 0
|
|
554
|
+
await refreshMembers(term, st)
|
|
555
|
+
st.screen = 'caps'
|
|
556
|
+
} else if (key.name === 'f5') {
|
|
516
557
|
await refreshDevices(term, st)
|
|
517
558
|
}
|
|
518
559
|
return true
|
|
@@ -525,6 +566,47 @@ async function beginPairing (term, st, profile) {
|
|
|
525
566
|
return r.ok
|
|
526
567
|
}
|
|
527
568
|
|
|
569
|
+
/**
|
|
570
|
+
* Enter marca o desmarca un permiso y lo aplica en el acto. Sin botón de «guardar»: cada
|
|
571
|
+
* cambio se sella en el acta y se avisa a los demás aparatos, así que acumularlos en
|
|
572
|
+
* pantalla solo serviría para que el acta y lo que ves dijeran cosas distintas.
|
|
573
|
+
*/
|
|
574
|
+
async function onKeyCaps (term, st, key) {
|
|
575
|
+
const i = L(st)
|
|
576
|
+
const rows = capsRows(st, term.t)
|
|
577
|
+
const sels = rows.filter((r) => r.sel).map((r) => r.meta)
|
|
578
|
+
moveSel(st, key, 'caps', sels.length)
|
|
579
|
+
const cur = sels[Math.min(st.sel.caps || 0, sels.length - 1)]
|
|
580
|
+
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
581
|
+
|
|
582
|
+
if (key.name === 'escape' || ch === 'b') { st.screen = 'devices'; st.capsFor = null; return true }
|
|
583
|
+
if (key.name === 'f5') { await refreshMembers(term, st); return true }
|
|
584
|
+
if ((key.name !== 'enter' && ch !== ' ') || !cur) return true
|
|
585
|
+
|
|
586
|
+
const miembro = (st.members || []).find((m) => m.pub === st.capsFor?.pub)
|
|
587
|
+
if (!miembro) return true
|
|
588
|
+
const caps = new Set(miembro.caps || [])
|
|
589
|
+
const dando = !caps.has(cur.cap)
|
|
590
|
+
if (dando) caps.add(cur.cap); else caps.delete(cur.cap)
|
|
591
|
+
|
|
592
|
+
const aplicar = async () => {
|
|
593
|
+
const r = await guard(term, st, i.applyingCaps, () => vc.setDeviceCaps(miembro.pub, [...caps], activeId(st)))
|
|
594
|
+
if (!r.ok) return
|
|
595
|
+
st.devices = r.v
|
|
596
|
+
await refreshMembers(term, st)
|
|
597
|
+
flash(st, dando ? i.capGiven(i.capName[cur.cap]) : i.capTaken(i.capName[cur.cap]))
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Administrar se PREGUNTA: es el permiso que deja a ese aparato admitir y expulsar
|
|
601
|
+
// dispositivos sin pasar por aquí. Los otros tres se marcan y ya.
|
|
602
|
+
if (cur.cap === 'admin' && dando) {
|
|
603
|
+
setConfirm(st, { text: i.confirmAdmin(st.capsFor.deviceId), onYes: aplicar })
|
|
604
|
+
return true
|
|
605
|
+
}
|
|
606
|
+
await aplicar()
|
|
607
|
+
return true
|
|
608
|
+
}
|
|
609
|
+
|
|
528
610
|
async function onKeyPairMode (term, st, key) {
|
|
529
611
|
const i = L(st)
|
|
530
612
|
const rows = pairModeRows(st, term.t)
|
|
@@ -620,7 +702,7 @@ async function onKeyPairing (term, st, key) {
|
|
|
620
702
|
* que usas, no en la máquina donde vive la bóveda.
|
|
621
703
|
*/
|
|
622
704
|
async function onKeyMe (term, st, key) {
|
|
623
|
-
if (key.name === '
|
|
705
|
+
if (key.name === 'f5') await refreshMe(term, st)
|
|
624
706
|
return true
|
|
625
707
|
}
|
|
626
708
|
|
|
@@ -657,7 +739,7 @@ async function onKeySecrets (term, st, key) {
|
|
|
657
739
|
onNo: () => { st.confirm = null }
|
|
658
740
|
})
|
|
659
741
|
}
|
|
660
|
-
} else if (
|
|
742
|
+
} else if (key.name === 'f5') {
|
|
661
743
|
await refreshSecrets(term, st)
|
|
662
744
|
}
|
|
663
745
|
return true
|
|
@@ -729,19 +811,34 @@ async function onConfirmKey (st, key) {
|
|
|
729
811
|
const INNER_TABS = ['devices', 'secrets', 'me']
|
|
730
812
|
const tabLabel = (i, k) => ({ devices: i.tabDevices, secrets: i.tabSecrets, me: i.tabMe })[k]
|
|
731
813
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
814
|
+
/**
|
|
815
|
+
* Las teclas que se pueden usar AHORA, no el catálogo entero. Aprobar/rechazar sin nadie
|
|
816
|
+
* esperando, o revocar sin un aparato seleccionado, no hacen nada: anunciarlas confunde
|
|
817
|
+
* («¿por qué no pasa nada?») y además quema esas letras para otros usos en la pantalla.
|
|
818
|
+
*/
|
|
819
|
+
const helpSegs = (i, screen, st = {}) => {
|
|
820
|
+
const segs = {
|
|
821
|
+
profiles: i.helpProfiles,
|
|
822
|
+
devices: i.helpDevices,
|
|
823
|
+
secrets: i.helpSecrets,
|
|
824
|
+
pairing: i.helpPairing,
|
|
825
|
+
pairmode: i.helpPairMode,
|
|
826
|
+
me: i.helpMe,
|
|
827
|
+
caps: i.helpCaps
|
|
828
|
+
}[screen] || []
|
|
829
|
+
if (typeof segs !== 'function') return segs
|
|
830
|
+
return segs({
|
|
831
|
+
pendiente: !!st.pending,
|
|
832
|
+
hayAparatos: (st.devices?.issued || []).length > 0,
|
|
833
|
+
haySecretos: Object.keys(st.secrets || {}).length > 0
|
|
834
|
+
})
|
|
835
|
+
}
|
|
740
836
|
|
|
741
837
|
const title = (i, screen) => ({
|
|
742
838
|
profiles: i.titleProfiles,
|
|
743
839
|
pairing: i.titlePairing,
|
|
744
|
-
pairmode: i.titlePairMode
|
|
840
|
+
pairmode: i.titlePairMode,
|
|
841
|
+
caps: i.titleCaps
|
|
745
842
|
})[screen] || ''
|
|
746
843
|
|
|
747
844
|
/** Barra de pestañas horizontal (Dispositivos | Scopes y variables) de la bóveda entrada. */
|
|
@@ -827,6 +924,7 @@ function render (term, st) {
|
|
|
827
924
|
else if (st.screen === 'devices') body = renderList(deviceRows(st, t), st.sel.devices, contentH, cols, t, scrollRef)
|
|
828
925
|
else if (st.screen === 'secrets') body = renderList(secretRows(st, t), st.sel.secrets, contentH, cols, t, scrollRef)
|
|
829
926
|
else if (st.screen === 'me') body = renderList(meRows(st, t), -1, contentH, cols, t, scrollRef)
|
|
927
|
+
else if (st.screen === 'caps') body = renderList(capsRows(st, t), st.sel.caps || 0, contentH, cols, t, scrollRef)
|
|
830
928
|
else if (st.screen === 'pairmode') body = renderList(pairModeRows(st, t), st.sel.pairmode, contentH, cols, t, scrollRef)
|
|
831
929
|
else if (st.screen === 'pairing') {
|
|
832
930
|
const pb = pairingBody(st, t, cols, contentH)
|
|
@@ -851,7 +949,7 @@ function render (term, st) {
|
|
|
851
949
|
} else lines[statusRow] = ''
|
|
852
950
|
|
|
853
951
|
// barra de ayuda
|
|
854
|
-
let help = fitHelp(helpSegs(i, st.screen), cols)
|
|
952
|
+
let help = fitHelp(helpSegs(i, st.screen, st), cols)
|
|
855
953
|
if (st.input) help = i.helpInput
|
|
856
954
|
else if (st.confirm) help = i.helpConfirm
|
|
857
955
|
lines[rows - 1] = t.bar(help, cols)
|
|
@@ -980,6 +1078,7 @@ export async function runTui () {
|
|
|
980
1078
|
else if (st.screen === 'devices') running = await onKeyDevices(term, st, key)
|
|
981
1079
|
else if (st.screen === 'secrets') running = await onKeySecrets(term, st, key)
|
|
982
1080
|
else if (st.screen === 'me') running = await onKeyMe(term, st, key)
|
|
1081
|
+
else if (st.screen === 'caps') running = await onKeyCaps(term, st, key)
|
|
983
1082
|
else if (st.screen === 'pairmode') running = await onKeyPairMode(term, st, key)
|
|
984
1083
|
else if (st.screen === 'pairing') running = await onKeyPairing(term, st, key)
|
|
985
1084
|
}
|
|
@@ -989,4 +1088,4 @@ export async function runTui () {
|
|
|
989
1088
|
}
|
|
990
1089
|
|
|
991
1090
|
// Solo para pruebas headless (render sin terminal real). No usar en runtime.
|
|
992
|
-
export const __test = { render, profileRows, deviceRows, secretRows, meRows, pairModeRows, pairingBody, scrollBody, fitHelp, toggleLang }
|
|
1091
|
+
export const __test = { render, profileRows, deviceRows, secretRows, meRows, capsRows, pairModeRows, pairingBody, scrollBody, fitHelp, toggleLang }
|
package/src/tui/i18n.js
CHANGED
|
@@ -35,11 +35,34 @@ const es = {
|
|
|
35
35
|
tabDevices: 'Dispositivos',
|
|
36
36
|
tabSecrets: 'Scopes y variables',
|
|
37
37
|
tabMe: 'Perfil',
|
|
38
|
+
// PERMISOS de un dispositivo (§9.1: se dice el beneficio, no el scope del cert).
|
|
39
|
+
titleCaps: 'Permisos del dispositivo',
|
|
40
|
+
capsFor: (id, nombre) => `Permisos de ${id}${nombre ? ' · ' + nombre : ''}`,
|
|
41
|
+
capsNoMember: ' (este dispositivo ya no está en el acta)',
|
|
42
|
+
capsApplyHint: 'Enter marca o desmarca. Cada cambio se aplica y se avisa a tus otros aparatos.',
|
|
43
|
+
capName: {
|
|
44
|
+
sign: 'Firmar como tú',
|
|
45
|
+
store: 'Guardar tus datos',
|
|
46
|
+
read: 'Leer tus datos',
|
|
47
|
+
admin: 'Administrar el perfil'
|
|
48
|
+
},
|
|
49
|
+
capHint: {
|
|
50
|
+
sign: 'usar tu identidad en las apps del ecosistema',
|
|
51
|
+
store: 'escribir en tu bóveda (perfil, contenido, datos sensibles)',
|
|
52
|
+
read: 'ver lo que guardaste',
|
|
53
|
+
admin: 'conectar y quitar dispositivos desde ese aparato, sin venir aquí'
|
|
54
|
+
},
|
|
55
|
+
confirmAdmin: (id) => `¿Dejar que ${id} conecte y quite dispositivos sin venir aquí?`,
|
|
56
|
+
capGiven: (n) => `Concedido: ${n}`,
|
|
57
|
+
capTaken: (n) => `Quitado: ${n}`,
|
|
58
|
+
applyingCaps: 'Aplicando…',
|
|
59
|
+
loadingMembers: 'Cargando el acta…',
|
|
60
|
+
helpCaps: ['↑↓', 'Enter marcar', 'F5 refrescar', 'Esc dispositivos', 'l English', 'q salir'],
|
|
38
61
|
// Perfil del usuario (lo que sincronizan los dispositivos). Solo lectura: se edita en
|
|
39
62
|
// el aparato, no en la máquina donde vive la bóveda.
|
|
40
63
|
loadingProfile: 'Cargando el perfil…',
|
|
41
64
|
noProfile: ' (esta bóveda todavía no tiene perfil)',
|
|
42
|
-
noProfileHint: ' Edita tu nombre o tu foto en un dispositivo emparejado y pulsa
|
|
65
|
+
noProfileHint: ' Edita tu nombre o tu foto en un dispositivo emparejado y pulsa F5.',
|
|
43
66
|
profileUpdated: (cuando) => ` actualizado ${cuando}`,
|
|
44
67
|
fieldName: 'nombre',
|
|
45
68
|
fieldPhoto: 'foto',
|
|
@@ -53,7 +76,7 @@ const es = {
|
|
|
53
76
|
hidden: ' (oculto)',
|
|
54
77
|
noName: '(sin nombre)',
|
|
55
78
|
no: 'no',
|
|
56
|
-
helpMe: ['←→ pestaña', '
|
|
79
|
+
helpMe: ['←→ pestaña', 'F5 refrescar', 'Esc bóvedas', 'l English', 'q salir'],
|
|
57
80
|
tabsHint: ' (←→ cambiar)',
|
|
58
81
|
titleProfiles: 'Bóvedas',
|
|
59
82
|
titlePairing: 'Emparejar un dispositivo',
|
|
@@ -175,12 +198,24 @@ const es = {
|
|
|
175
198
|
// language/quit); lo único que se traduce es la palabra que las explica.
|
|
176
199
|
// Segmentos, no una línea: el render recorta del medio si no caben.
|
|
177
200
|
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
|
-
|
|
201
|
+
// La barra dice lo que se PUEDE hacer AHORA, no todo lo que existe: aprobar/rechazar solo
|
|
202
|
+
// valen si hay alguien esperando, y renombrar/revocar solo si hay un aparato seleccionado.
|
|
203
|
+
// Anunciar teclas muertas confunde y además las quema para otros usos.
|
|
204
|
+
helpDevices: ({ pendiente, hayAparatos } = {}) => [
|
|
205
|
+
'←→ pestaña', '↑↓', 'p emparejar',
|
|
206
|
+
...(pendiente ? ['a aprobar', 'x rechazar'] : []),
|
|
207
|
+
...(hayAparatos ? ['r renombrar', 'c permisos', 'v revocar'] : []),
|
|
208
|
+
'F5 refrescar', 'Esc bóvedas', 'l English', 'q salir'
|
|
209
|
+
],
|
|
179
210
|
renameDeviceLabel: (id) => `¿Cómo quieres llamar a ${id}?`,
|
|
180
211
|
renameDeviceHint: 'el nombre con el que lo reconoces (Esc cancela)',
|
|
181
212
|
renaming: 'Renombrando…',
|
|
182
213
|
deviceRenamed: (n) => `Ahora se llama «${n}»`,
|
|
183
|
-
helpSecrets:
|
|
214
|
+
helpSecrets: ({ haySecretos } = {}) => [
|
|
215
|
+
'←→ pestaña', '↑↓', 'n nueva variable',
|
|
216
|
+
...(haySecretos ? ['x quitar (variable/scope)'] : []),
|
|
217
|
+
'F5 refrescar', 'Esc bóvedas', 'l English', 'q salir'
|
|
218
|
+
],
|
|
184
219
|
helpPairing: ['a aprobar', 'x rechazar', 'r reiniciar', '↑↓ scroll', 'Esc atrás', 'l English'],
|
|
185
220
|
helpPairMode: ['↑↓', 'Enter elegir', 'Esc atrás', 'l English', 'q salir'],
|
|
186
221
|
|
|
@@ -228,9 +263,31 @@ const en = {
|
|
|
228
263
|
tabDevices: 'Devices',
|
|
229
264
|
tabSecrets: 'Scopes & variables',
|
|
230
265
|
tabMe: 'Profile',
|
|
266
|
+
titleCaps: 'Device permissions',
|
|
267
|
+
capsFor: (id, nombre) => `Permissions for ${id}${nombre ? ' · ' + nombre : ''}`,
|
|
268
|
+
capsNoMember: ' (this device is no longer in the record)',
|
|
269
|
+
capsApplyHint: 'Enter ticks or unticks. Each change applies and your other devices are told.',
|
|
270
|
+
capName: {
|
|
271
|
+
sign: 'Sign as you',
|
|
272
|
+
store: 'Save your data',
|
|
273
|
+
read: 'Read your data',
|
|
274
|
+
admin: 'Manage the profile'
|
|
275
|
+
},
|
|
276
|
+
capHint: {
|
|
277
|
+
sign: 'use your identity across the ecosystem apps',
|
|
278
|
+
store: 'write to your vault (profile, content, sensitive data)',
|
|
279
|
+
read: 'see what you saved',
|
|
280
|
+
admin: 'connect and remove devices from that device, without coming here'
|
|
281
|
+
},
|
|
282
|
+
confirmAdmin: (id) => `Let ${id} connect and remove devices without coming here?`,
|
|
283
|
+
capGiven: (n) => `Granted: ${n}`,
|
|
284
|
+
capTaken: (n) => `Removed: ${n}`,
|
|
285
|
+
applyingCaps: 'Applying…',
|
|
286
|
+
loadingMembers: 'Loading the record…',
|
|
287
|
+
helpCaps: ['↑↓', 'Enter tick', 'F5 refresh', 'Esc devices', 'l Español', 'q quit'],
|
|
231
288
|
loadingProfile: 'Loading profile…',
|
|
232
289
|
noProfile: ' (this vault has no profile yet)',
|
|
233
|
-
noProfileHint: ' Edit your name or photo on a paired device and press
|
|
290
|
+
noProfileHint: ' Edit your name or photo on a paired device and press F5.',
|
|
234
291
|
profileUpdated: (cuando) => ` updated ${cuando}`,
|
|
235
292
|
fieldName: 'name',
|
|
236
293
|
fieldPhoto: 'photo',
|
|
@@ -244,7 +301,7 @@ const en = {
|
|
|
244
301
|
hidden: ' (hidden)',
|
|
245
302
|
noName: '(no name)',
|
|
246
303
|
no: 'no',
|
|
247
|
-
helpMe: ['←→ tab', '
|
|
304
|
+
helpMe: ['←→ tab', 'F5 refresh', 'Esc vaults', 'l Español', 'q quit'],
|
|
248
305
|
tabsHint: ' (←→ switch)',
|
|
249
306
|
titleProfiles: 'Vaults',
|
|
250
307
|
titlePairing: 'Pair a device',
|
|
@@ -355,12 +412,21 @@ const en = {
|
|
|
355
412
|
helpConfirm: 'y confirm · n/Esc cancel',
|
|
356
413
|
|
|
357
414
|
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'],
|
|
358
|
-
helpDevices:
|
|
415
|
+
helpDevices: ({ pendiente, hayAparatos } = {}) => [
|
|
416
|
+
'←→ tab', '↑↓', 'p pair',
|
|
417
|
+
...(pendiente ? ['a approve', 'x reject'] : []),
|
|
418
|
+
...(hayAparatos ? ['r rename', 'c permissions', 'v revoke'] : []),
|
|
419
|
+
'F5 refresh', 'Esc vaults', 'l Español', 'q quit'
|
|
420
|
+
],
|
|
359
421
|
renameDeviceLabel: (id) => `What do you want to call ${id}?`,
|
|
360
422
|
renameDeviceHint: 'the name you recognise it by (Esc cancels)',
|
|
361
423
|
renaming: 'Renaming…',
|
|
362
424
|
deviceRenamed: (n) => `Now called "${n}"`,
|
|
363
|
-
helpSecrets:
|
|
425
|
+
helpSecrets: ({ haySecretos } = {}) => [
|
|
426
|
+
'←→ tab', '↑↓', 'n new variable',
|
|
427
|
+
...(haySecretos ? ['x remove (variable/scope)'] : []),
|
|
428
|
+
'F5 refresh', 'Esc vaults', 'l Español', 'q quit'
|
|
429
|
+
],
|
|
364
430
|
helpPairing: ['a approve', 'x reject', 'r restart', '↑↓ scroll', 'Esc back', 'l Español'],
|
|
365
431
|
helpPairMode: ['↑↓', 'Enter choose', 'Esc back', 'l Español', 'q quit'],
|
|
366
432
|
|
package/src/tui/term.js
CHANGED
|
@@ -124,7 +124,13 @@ export function makeTheme () {
|
|
|
124
124
|
|
|
125
125
|
// --------------------------------- teclado ----------------------------------
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Decodifica un trozo de stdin en teclas. Exportado SOLO para poder probarlo: vivía
|
|
129
|
+
* encerrado en `createTerm`, así que las secuencias de escape (flechas, F5, supr) no las
|
|
130
|
+
* comprobaba nadie — y una tabla mal escrita ahí no falla, simplemente la tecla no hace
|
|
131
|
+
* nada, que es la avería más difícil de ver.
|
|
132
|
+
*/
|
|
133
|
+
export function parseChunk (s, push) {
|
|
128
134
|
let i = 0
|
|
129
135
|
const arrow = { A: 'up', B: 'down', C: 'right', D: 'left', H: 'home', F: 'end' }
|
|
130
136
|
while (i < s.length) {
|
|
@@ -138,7 +144,9 @@ function parseChunk (s, push) {
|
|
|
138
144
|
let j = i + 2; let num = ''
|
|
139
145
|
while (j < s.length && /[0-9;]/.test(s[j])) { num += s[j]; j++ }
|
|
140
146
|
const fin = s[j]
|
|
141
|
-
|
|
147
|
+
// F5 = refrescar. Es tecla de función y no consume un mnemónico: así `r` queda
|
|
148
|
+
// libre para renombrar, que es lo que significa en el resto de la TUI.
|
|
149
|
+
const seq = { 3: 'delete', 5: 'pageup', 6: 'pagedown', 1: 'home', 4: 'end', 15: 'f5' }
|
|
142
150
|
if (fin === '~' && seq[num]) { push({ name: seq[num] }); i = j + 1; continue }
|
|
143
151
|
i = (fin ? j + 1 : s.length); continue
|
|
144
152
|
}
|
package/src/vaultControl.js
CHANGED
|
@@ -33,12 +33,14 @@ const F = {
|
|
|
33
33
|
profilesList: 'profiles-list.json',
|
|
34
34
|
secretsList: 'secrets-list.json',
|
|
35
35
|
me: 'me.json',
|
|
36
|
+
acta: 'acta.json',
|
|
36
37
|
// peticiones (las escribe el control; el daemon las consume y borra)
|
|
37
38
|
pairReq: 'pair-request.json',
|
|
38
39
|
approveReq: 'approve-request.json',
|
|
39
40
|
rejectReq: 'reject-request.json',
|
|
40
41
|
revokeReq: 'revoke-request.json',
|
|
41
42
|
labelReq: 'label-request.json',
|
|
43
|
+
capsReq: 'caps-request.json',
|
|
42
44
|
secretReq: 'secret-request.json',
|
|
43
45
|
profileReq: 'profile-request.json',
|
|
44
46
|
meReq: 'me-request.json',
|
|
@@ -212,6 +214,37 @@ export async function setDeviceLabel (pub, label, profile) {
|
|
|
212
214
|
return listDevices(profile)
|
|
213
215
|
}
|
|
214
216
|
|
|
217
|
+
/**
|
|
218
|
+
* El ACTA del perfil: quién es miembro y qué puede hacer cada uno. Es lo que manda para los
|
|
219
|
+
* permisos — la lista de dispositivos enseña el SCOPE DEL CERT, que es su reflejo y puede
|
|
220
|
+
* ir por detrás hasta que el aparato renueve.
|
|
221
|
+
*/
|
|
222
|
+
export async function listMembers (profile) {
|
|
223
|
+
requireAlive()
|
|
224
|
+
rm(F.acta)
|
|
225
|
+
writeReq(F.dumpReq, {}, profile)
|
|
226
|
+
signalOrCleanup('SIGUSR2', [F.dumpReq])
|
|
227
|
+
const d = await waitFor(F.acta)
|
|
228
|
+
if (!d) throw coded('the daemon did not reply', 'NO_REPLY')
|
|
229
|
+
return d.members || []
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Cambia lo que PUEDE hacer un dispositivo. La lista es completa (no un delta): lo que no
|
|
234
|
+
* venga, se le quita.
|
|
235
|
+
*
|
|
236
|
+
* `admin` (administrar el perfil a distancia) se concede AQUÍ, en la máquina de la bóveda,
|
|
237
|
+
* y nunca al emparejar: así el QR que circula no puede otorgarla nunca, y darla es un
|
|
238
|
+
* gesto deliberado del dueño que queda escrito en el acta.
|
|
239
|
+
*/
|
|
240
|
+
export async function setDeviceCaps (pub, caps, profile) {
|
|
241
|
+
requireAlive()
|
|
242
|
+
writeReq(F.capsReq, { pub, caps }, profile)
|
|
243
|
+
signalOrCleanup('SIGUSR2', [F.capsReq])
|
|
244
|
+
await sleep(600)
|
|
245
|
+
return listDevices(profile)
|
|
246
|
+
}
|
|
247
|
+
|
|
215
248
|
/** Revoca un dispositivo por su `nonce` (le ordena autoborrarse) y revuelca. */
|
|
216
249
|
export async function revokeDevice (nonce, profile) {
|
|
217
250
|
requireAlive()
|