@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vaultd",
3
- "version": "0.17.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 === 'n' && cur) {
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 === 'r') {
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 === 'char' && key.ch.toLowerCase() === 'r') await refreshMe(term, st)
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 (ch === 'r') {
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
- const helpSegs = (i, screen) => ({
733
- profiles: i.helpProfiles,
734
- devices: i.helpDevices,
735
- secrets: i.helpSecrets,
736
- pairing: i.helpPairing,
737
- pairmode: i.helpPairMode,
738
- me: i.helpMe
739
- })[screen] || []
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 R.',
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', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
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
- helpDevices: ['←→ pestaña', '↑↓', 'p emparejar', 'a aprobar', 'x rechazar', 'n renombrar', 'v revocar', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
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: ['←→ pestaña', '↑↓', 'n nueva variable', 'x quitar (variable/scope)', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
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 R.',
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', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
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: ['←→ tab', '↑↓', 'p pair', 'a approve', 'x reject', 'n rename', 'v revoke', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
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: ['←→ tab', '↑↓', 'n new variable', 'x remove (variable/scope)', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
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
- function parseChunk (s, push) {
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
- const seq = { 3: 'delete', 5: 'pageup', 6: 'pagedown', 1: 'home', 4: 'end' }
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
  }
@@ -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()