@dotrino/vaultd 0.78.0 → 0.80.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/env.js +58 -11
- package/package.json +2 -2
- package/src/ctl.js +40 -33
- package/src/daemon.js +0 -7
- package/src/passwords.js +7 -2
- package/src/tui/app.js +76 -28
- package/src/tui/i18n.js +20 -8
- package/src/vault.js +29 -19
package/lib/src/env.js
CHANGED
|
@@ -29,18 +29,63 @@ export function serviceRoot () {
|
|
|
29
29
|
return process.env.DOTRINO_ENV_HOME || path.join(os.homedir(), '.dotrino', 'service')
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* DE QUÉ BÓVEDA VINO ESTO: `~/.dotrino/service/<bóveda>/<ns>`.
|
|
34
|
+
*
|
|
35
|
+
* Es una ETIQUETA TUYA, de esta máquina: nadie la manda por la red, no entra en el acta y
|
|
36
|
+
* no se compara con nada — puedes llamarla como quieras. Sirve solo para que dos bóvedas
|
|
37
|
+
* puedan tener un cajón que se llame igual.
|
|
38
|
+
*
|
|
39
|
+
* Se llama `--vault` y no `--profile` a propósito (dueño): `dotrino-vault --profile` ya
|
|
40
|
+
* significa OTRA cosa —el perfil de la bóveda— y los dos comandos se usan seguidos. Dos
|
|
41
|
+
* banderas con el mismo nombre y distinto significado se confunden el día que hay prisa.
|
|
42
|
+
*
|
|
43
|
+
* Antes la ruta era `<raíz>/<ns>` y el nombre del cajón era una CLAVE GLOBAL del usuario:
|
|
44
|
+
* si dos bóvedas usaban `aws`, el segundo enrolamiento pisaba al primero y lo dejaba
|
|
45
|
+
* inservible —avisaba, pero el papel del primero seguía existiendo en su bóveda sin
|
|
46
|
+
* identidad en disco que lo usara—. Obligar a nombrar el perfil quita esa colisión de
|
|
47
|
+
* raíz en vez de pedirle al usuario que se acuerde de no repetir nombres.
|
|
48
|
+
*
|
|
49
|
+
* Mismo alfabeto que el `ns` porque es lo mismo: un segmento de ruta.
|
|
50
|
+
*/
|
|
51
|
+
export const isValidVaultLabel = (p) => typeof p === 'string' && /^[a-z0-9-]{1,32}$/.test(p)
|
|
52
|
+
|
|
53
|
+
/** Directorio de la identidad del servicio `ns` bajo la etiqueta de bóveda `vault`. */
|
|
54
|
+
export function serviceDir (vault, ns) {
|
|
34
55
|
if (process.env.DOTRINO_ENV_DIR) return process.env.DOTRINO_ENV_DIR
|
|
56
|
+
if (!isValidVaultLabel(vault)) {
|
|
57
|
+
throw new Error('missing vault label: pass --vault <nombre> (use [a-z0-9-]{1,32}). It is a label of YOUR machine: it says which vault this drawer came from')
|
|
58
|
+
}
|
|
35
59
|
if (!isValidSecretsNs(ns)) throw new Error('invalid ns (use [a-z0-9-]{1,32}, e.g. "myapp")')
|
|
36
|
-
return path.join(serviceRoot(), ns)
|
|
60
|
+
return path.join(serviceRoot(), vault, ns)
|
|
37
61
|
}
|
|
38
62
|
|
|
39
|
-
/**
|
|
63
|
+
/** La etiqueta de bóveda: `--vault`, o `DOTRINO_ENV_VAULT`. */
|
|
64
|
+
export const resolveVaultLabel = (v) => v || process.env.DOTRINO_ENV_VAULT || null
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Lo enrolado en esta máquina: `[{ vault, ns }]`. Recorre DOS niveles.
|
|
68
|
+
*
|
|
69
|
+
* Y avisa de lo que quedó en el nivel de antes (`<raíz>/<ns>`): no se lee ni se usa —
|
|
70
|
+
* repescarlo en silencio sería fingir que la migración no hace falta— pero callarlo
|
|
71
|
+
* dejaría al dueño con un servicio que «desapareció» sin explicación.
|
|
72
|
+
*/
|
|
40
73
|
export function listEnrolled () {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
74
|
+
const out = []
|
|
75
|
+
let dirs = []
|
|
76
|
+
try { dirs = fs.readdirSync(serviceRoot(), { withFileTypes: true }) } catch (_) { return out }
|
|
77
|
+
for (const d of dirs) {
|
|
78
|
+
if (!d.isDirectory() || !isValidVaultLabel(d.name)) continue
|
|
79
|
+
// Un enrolamiento del formato viejo es un directorio con `service-identity.json`
|
|
80
|
+
// DENTRO; uno del nuevo tiene subdirectorios. Así se distinguen sin adivinar.
|
|
81
|
+
if (readServiceIdentity(path.join(serviceRoot(), d.name))) { out.push({ legacy: true, ns: d.name }); continue }
|
|
82
|
+
let hijos = []
|
|
83
|
+
try { hijos = fs.readdirSync(path.join(serviceRoot(), d.name)) } catch (_) { continue }
|
|
84
|
+
for (const ns of hijos) {
|
|
85
|
+
if (isValidSecretsNs(ns) && readServiceIdentity(path.join(serviceRoot(), d.name, ns))) out.push({ vault: d.name, ns })
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out
|
|
44
89
|
}
|
|
45
90
|
|
|
46
91
|
/**
|
|
@@ -98,10 +143,12 @@ let lastApplied = null
|
|
|
98
143
|
* `overridden` = las que YA tenían otro valor en el entorno y el vault pisó. Es
|
|
99
144
|
* el dato que delata un `.env` rancio, así que se reporta en vez de callarse.
|
|
100
145
|
*/
|
|
101
|
-
export async function loadEnv ({ ns, dir, override, wait = true, required = [], onRetry, onPending } = {}) {
|
|
146
|
+
export async function loadEnv ({ ns, vault, dir, override, wait = true, required = [], onRetry, onPending } = {}) {
|
|
102
147
|
if (override === undefined) override = overrideByDefault()
|
|
103
148
|
ns = resolveNs(ns)
|
|
104
|
-
|
|
149
|
+
// La ETIQUETA DE BÓVEDA es obligatoria salvo que se diga el directorio a pelo: es lo que
|
|
150
|
+
// hace que dos bóvedas puedan tener un cajón con el mismo nombre sin pisarse.
|
|
151
|
+
dir = dir || serviceDir(resolveVaultLabel(vault), ns)
|
|
105
152
|
const load = wait ? waitForSecrets : fetchSecrets
|
|
106
153
|
const secrets = await load({ dir, ns, onRetry, onPending })
|
|
107
154
|
|
|
@@ -199,9 +246,9 @@ export function applyEnv (secrets, override = overrideByDefault()) {
|
|
|
199
246
|
* también. Se elige 0 porque salir a propósito no es un fallo.
|
|
200
247
|
* @returns {Promise<{stop:()=>void}>}
|
|
201
248
|
*/
|
|
202
|
-
export async function watchEnv ({ ns, dir, applied = lastApplied ?? undefined, onUpdate, exitCode = 0, quiet = false, ...rest } = {}) {
|
|
249
|
+
export async function watchEnv ({ ns, vault, dir, applied = lastApplied ?? undefined, onUpdate, exitCode = 0, quiet = false, ...rest } = {}) {
|
|
203
250
|
ns = resolveNs(ns)
|
|
204
|
-
dir = dir || serviceDir(ns)
|
|
251
|
+
dir = dir || serviceDir(resolveVaultLabel(vault), ns)
|
|
205
252
|
const say = (m) => { if (!quiet) console.error(m) }
|
|
206
253
|
|
|
207
254
|
const exitNow = (reason) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.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": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"node": ">=20"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@dotrino/identity": "0.
|
|
23
|
+
"@dotrino/identity": "0.76.0",
|
|
24
24
|
"@dotrino/passmanager": "^0.9.0",
|
|
25
25
|
"@dotrino/proxy-client": "^0.13.1",
|
|
26
26
|
"ws": "^8.18.0"
|
package/src/ctl.js
CHANGED
|
@@ -202,9 +202,10 @@ async function cmdPair (args = []) {
|
|
|
202
202
|
// `secrets:<ns>`, y los dos se combinan (`--service eco --scope sign` = un bot que
|
|
203
203
|
// firma como aparato del acta y lee solo su cajón). `admin` no se empareja: se
|
|
204
204
|
// concede desde el PC (`caps <ID> +administra`).
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
205
|
+
// (Aquí vivía `--approval`. Ya no hace falta: PEDIR APROBACIÓN ES EL DEFECTO, y lo
|
|
206
|
+
// contrario —`unattended`— no se empareja, por el mismo motivo que `admin`: un QR que
|
|
207
|
+
// circula no puede conceder «llévate mis claves privadas sin preguntar». Se concede
|
|
208
|
+
// después y a mano con `caps <ID> +desatendido`.)
|
|
208
209
|
// --name <n>: CÓMO SE VA A LLAMAR el aparato que entre, decidido aquí y antes de nada.
|
|
209
210
|
// Sin esto el nombre lo ponía el propio aparato, que por defecto usa el apodo del
|
|
210
211
|
// PERFIL: acababas con varios dispositivos llamados igual que tú, y para distinguirlos
|
|
@@ -311,7 +312,7 @@ async function cmdPair (args = []) {
|
|
|
311
312
|
|
|
312
313
|
// La petición se escribe SIEMPRE (aunque no haya --service): lleva a qué perfil
|
|
313
314
|
// se empareja el dispositivo.
|
|
314
|
-
writeReq('pair-request.json', { ...(service ? { service } : {}), ...(scope ? { scope } : {}), ...(adopt ? { mode: 'adopt' } : {}), ...(
|
|
315
|
+
writeReq('pair-request.json', { ...(service ? { service } : {}), ...(scope ? { scope } : {}), ...(adopt ? { mode: 'adopt' } : {}), ...(admin ? { admin: true } : {}), ...(label ? { label } : {}) })
|
|
315
316
|
sendSignal(s.pid, 'SIGUSR1')
|
|
316
317
|
|
|
317
318
|
let pair = null
|
|
@@ -545,7 +546,11 @@ async function cmdMembers () {
|
|
|
545
546
|
const CAP = {
|
|
546
547
|
sign: 'firma', store: 'guarda', read: 'lee', secrets: 'lee sus claves',
|
|
547
548
|
admin: `${B}administra el perfil${Z}`, approve: `${B}aprueba pedidos${Z}`,
|
|
548
|
-
sealer: `${B}sella el acta${Z}`, passwords: `${B}pide contraseñas${Z}
|
|
549
|
+
sealer: `${B}sella el acta${Z}`, passwords: `${B}pide contraseñas${Z}`,
|
|
550
|
+
// Resaltado como los otros cuatro: dice si esa máquina se lleva tus claves privadas
|
|
551
|
+
// sola o si te lo pregunta. Sin traducción salía el identificador en crudo
|
|
552
|
+
// (`unattended`) en una lista que lee un humano.
|
|
553
|
+
unattended: `${B}recibe claves sin aprobación${Z}`
|
|
549
554
|
}
|
|
550
555
|
// El nombre del perfil es una pubkey JWK. Recortarla no la hace legible: la deja
|
|
551
556
|
// pareciendo un error (`{"key_ops":["verify"],"e…`). Se muestra su huella corta, la
|
|
@@ -575,7 +580,7 @@ async function cmdMembers () {
|
|
|
575
580
|
// se mira quién es quién — y en la lista de variables ya seria tarde.
|
|
576
581
|
if (m.cn && !m.canSeal) console.log(' %ssin llave de cifrado: NO puede leer sus variables%s', R, Z)
|
|
577
582
|
}
|
|
578
|
-
console.log('\n Cambiar permisos: dotrino-vault caps <ID> +firma | -firma | +guarda | -guarda | +lee | -lee | +administra | +aprueba | +contrasenas | +sella | +
|
|
583
|
+
console.log('\n Cambiar permisos: dotrino-vault caps <ID> +firma | -firma | +guarda | -guarda | +lee | -lee | +administra | +aprueba | +contrasenas | +sella | +desatendido')
|
|
579
584
|
console.log(' «Permiso»: ese aparato solo recibe claves privadas cuando lo apruebas desde un aparato con «aprueba» (en cada arranque).')
|
|
580
585
|
console.log(' «Administra» deja conectar y quitar dispositivos desde ese aparato, sin venir aquí.')
|
|
581
586
|
console.log(' No deja cambiar permisos ni traspasar el mando: eso solo se hace en esta máquina.')
|
|
@@ -617,28 +622,22 @@ async function findMember (id) {
|
|
|
617
622
|
return m
|
|
618
623
|
}
|
|
619
624
|
|
|
620
|
-
|
|
621
|
-
* `dotrino-vault approval <ID> on|off
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
+
/*
|
|
626
|
+
* Aquí vivía `dotrino-vault approval <ID> on|off`. Ya no existe: pedir aprobación para
|
|
627
|
+
* recibir claves privadas dejó de ser una marca local de esta bóveda y pasó a ser un
|
|
628
|
+
* PERMISO DEL ACTA (`unattended`, dueño 2026-09-01). Se concede y se quita como cualquier
|
|
629
|
+
* otro, y por lo tanto viaja, se ve en la pantalla de permisos y lo respeta cualquier
|
|
630
|
+
* bóveda de la cuenta:
|
|
631
|
+
*
|
|
632
|
+
* dotrino-vault caps <ID> +desatendido ← se lleva las claves solo
|
|
633
|
+
* dotrino-vault caps <ID> -desatendido ← vuelve a pedir permiso (el defecto)
|
|
625
634
|
*/
|
|
626
|
-
async function cmdApproval (args = []) {
|
|
627
|
-
const [id, val] = args
|
|
628
|
-
if (!id || !['on', 'off'].includes(val)) { console.error('uso: dotrino-vault approval <ID> on|off'); process.exit(2) }
|
|
629
|
-
const m = await findMember(id)
|
|
630
|
-
writeReq('secret-request.json', { op: 'approval', pub: m.pub, id: m.id, approval: val === 'on' })
|
|
631
|
-
sendSignal(requireDaemon().pid, 'SIGUSR2')
|
|
632
|
-
console.log(val === 'on'
|
|
633
|
-
? `Listo: ${m.id} solo recibirá claves privadas cuando un aparato con caps <ID> +aprueba lo apruebe (en cada arranque).`
|
|
634
|
-
: `Listo: ${m.id} vuelve a recibir sus claves sin aprobación.`)
|
|
635
|
-
}
|
|
636
635
|
|
|
637
636
|
/** `dotrino-vault caps <ID> ±permiso` — cambia lo que puede hacer un dispositivo. */
|
|
638
637
|
async function cmdCaps (args = []) {
|
|
639
638
|
const [id, ...changes] = args
|
|
640
639
|
if (!id || !changes.length) {
|
|
641
|
-
console.error('uso: dotrino-vault caps <ID> +firma|-firma|+guarda|-guarda|+lee|-lee|+administra|-administra|+aprueba|-aprueba|+contrasenas|-contrasenas|+sella|-sella|+
|
|
640
|
+
console.error('uso: dotrino-vault caps <ID> +firma|-firma|+guarda|-guarda|+lee|-lee|+administra|-administra|+aprueba|-aprueba|+contrasenas|-contrasenas|+sella|-sella|+desatendido|-desatendido')
|
|
642
641
|
process.exit(2)
|
|
643
642
|
}
|
|
644
643
|
const CAP_BY_WORD = {
|
|
@@ -650,25 +649,34 @@ async function cmdCaps (args = []) {
|
|
|
650
649
|
// `contraseñas`: el gestor (la extensión, la app del teléfono) puede PEDIR
|
|
651
650
|
// credenciales de a una. Se acepta con y sin tilde: nadie escribe la ñ en una CLI.
|
|
652
651
|
contrasenas: 'passwords', 'contraseñas': 'passwords',
|
|
652
|
+
// `desatendido`: RECIBE CLAVES PRIVADAS SIN QUE NADIE APRUEBE. Sin él, la bóveda no
|
|
653
|
+
// entrega nada hasta que un aparato con `aprueba` lo firme (una vez por arranque).
|
|
654
|
+
//
|
|
655
|
+
// No se llama `permiso` —como la marca local de antes— a propósito: aquella significaba
|
|
656
|
+
// «este aparato PIDE permiso» y esta significa lo contrario. Reusar la palabra con el
|
|
657
|
+
// sentido invertido es la clase de trampa que se paga una madrugada.
|
|
658
|
+
desatendido: 'unattended', unattended: 'unattended',
|
|
653
659
|
sign: 'sign', store: 'store', read: 'read', admin: 'admin', approve: 'approve',
|
|
654
660
|
passwords: 'passwords'
|
|
655
661
|
}
|
|
656
662
|
const s = requireDaemon()
|
|
657
663
|
const m = await findMember(id)
|
|
658
664
|
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
|
|
665
|
+
// El desvío de `+permiso` a una marca local de la bóveda se fue: ahora TODO es un
|
|
666
|
+
// permiso del acta y sigue el mismo camino, incluido `desatendido`.
|
|
667
|
+
//
|
|
668
|
+
// Y `permiso` se rechaza en voz alta en vez de ignorarse: significaba lo CONTRARIO
|
|
669
|
+
// («este aparato pide aprobación»), así que quien lo teclee de memoria estaría pidiendo
|
|
670
|
+
// justo lo opuesto a lo que cree.
|
|
662
671
|
for (const c of changes) {
|
|
663
672
|
const w = c.slice(1).toLowerCase()
|
|
664
|
-
if (
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
} else rest.push(c)
|
|
673
|
+
if (w === 'permiso' || w === 'approval') {
|
|
674
|
+
console.error('`%s` ya no existe. Pedir aprobación es el DEFECTO; lo que se concede es lo contrario:', w)
|
|
675
|
+
console.error(' dotrino-vault caps %s +desatendido ← se lleva las claves privadas sin aprobación', m.id)
|
|
676
|
+
process.exit(2)
|
|
677
|
+
}
|
|
670
678
|
}
|
|
671
|
-
|
|
679
|
+
const rest = changes
|
|
672
680
|
const caps = new Set(m.caps)
|
|
673
681
|
for (const c of rest) {
|
|
674
682
|
const sign = c[0]
|
|
@@ -1495,7 +1503,6 @@ function help () {
|
|
|
1495
1503
|
pair --name <nombre> cómo se llamará el aparato que entre. Sin esto el nombre lo pone
|
|
1496
1504
|
ÉL, y por defecto usa el apodo del perfil: acabas con varios
|
|
1497
1505
|
dispositivos llamados igual que tú
|
|
1498
|
-
pair --approval el aparato que entre pedirá tu aprobación (teléfono) al recibir claves
|
|
1499
1506
|
pair --admin el aparato que entre podrá ADMINISTRAR (es lo que es una consola).
|
|
1500
1507
|
El QR no lleva nada: el permiso se aplica al aprobar el código aquí
|
|
1501
1508
|
pair --quiet escupe SOLO la invitación (una línea) y termina: sin QR y sin
|
package/src/daemon.js
CHANGED
|
@@ -143,7 +143,6 @@ export async function runDaemon () {
|
|
|
143
143
|
|
|
144
144
|
// --- SIGUSR1: iniciar emparejamiento ---
|
|
145
145
|
const pairFile = path.join(dir, 'pair.json')
|
|
146
|
-
let pendingApproval = false // lo pidió `pair --approval`; se aplica al aprobar
|
|
147
146
|
/**
|
|
148
147
|
* Lo pidió `pair --admin`: el aparato que entre por ESTA invitación podrá administrar.
|
|
149
148
|
*
|
|
@@ -199,9 +198,6 @@ export async function runDaemon () {
|
|
|
199
198
|
// La etiqueta por defecto de un servicio es su ns a secas: la lista ya marca [servicio «ns»],
|
|
200
199
|
// y el prefijo «service:» confundía (el cajón se llama claude, no service:claude).
|
|
201
200
|
const label = pairReq?.label || (isService ? pairReq.service : 'cli')
|
|
202
|
-
// `pair --approval`: el aparato que entre por esta invitación pedirá el visto bueno del
|
|
203
|
-
// teléfono cada vez que reciba claves privadas (se aplica al aprobarlo, abajo).
|
|
204
|
-
pendingApproval = !!pairReq?.approval
|
|
205
201
|
pendingAdmin = !!pairReq?.admin
|
|
206
202
|
// `profile`/`profileName`: la CUENTA del vault a la que entra el dispositivo.
|
|
207
203
|
// Con varias bóvedas en el mismo daemon, el QR sale de UNA y quien empareja
|
|
@@ -392,8 +388,6 @@ export async function runDaemon () {
|
|
|
392
388
|
const vault = targetOf(appr)
|
|
393
389
|
if (!vault) throw Object.assign(new Error('profile locked'), { code: 'PROFILE_LOCKED' })
|
|
394
390
|
const r = await vault.approveDevice(appr.code); rm(pendingEnrollFile); rm(pairFile)
|
|
395
|
-
if (pendingApproval && r?.cert?.sub) { try { await vault.setApproval(r.cert.sub, true); console.log('[vault] the new device will need approval on every key request') } catch (e) { console.error('[vault] could not flag approval: %s', e.message) } }
|
|
396
|
-
pendingApproval = false
|
|
397
391
|
// `pair --admin`: se le SUMA `admin` a lo que ya tiene, no se le reescriben los
|
|
398
392
|
// permisos — el aparato acaba de entrar con el scope que pidió la invitación.
|
|
399
393
|
if (pendingAdmin && r?.deviceId) {
|
|
@@ -542,7 +536,6 @@ export async function runDaemon () {
|
|
|
542
536
|
console.log('[vault] %d secret(s) applied in one go: %s', changed.length, sec.pub ? 'device' : sec.ns)
|
|
543
537
|
} else if (sec.op === 'set') { await vault.setSecret(sec.ns, sec.key, sec.value, sec.public); console.log('[vault] secret saved: %s/%s', sec.ns, sec.key) }
|
|
544
538
|
else if (sec.op === 'rm') { await vault.deleteSecret(sec.ns, sec.key); console.log('[vault] secret deleted: %s/%s', sec.ns, sec.key) }
|
|
545
|
-
else if (sec.op === 'approval') { const r = await vault.setApproval(sec.pub, !!sec.approval); console.log('[vault] device %s: approval %s', sec.id || '?', r.approval ? 'REQUIRED on every key request' : 'off') }
|
|
546
539
|
else if (sec.op === 'dev-set') { await vault.setDeviceSecret(sec.pub, sec.key, sec.value, sec.public); console.log('[vault] device secret saved: %s', sec.key) }
|
|
547
540
|
else if (sec.op === 'dev-rm') { await vault.deleteDeviceSecret(sec.pub, sec.key); console.log('[vault] device secret deleted: %s', sec.key) }
|
|
548
541
|
// Saldar lo que quedó a deber: heredarle a un aparato nuevo lo ya guardado y
|
package/src/passwords.js
CHANGED
|
@@ -27,7 +27,8 @@ export const PASSWORDS_CAP = 'passwords'
|
|
|
27
27
|
* `cek` la llave de la bóveda de contraseñas (CryptoKey AES-GCM)
|
|
28
28
|
* `isAllowed(pubkey)` qué aparatos pueden pedir — lo decide el acta, no esto
|
|
29
29
|
* `encPubOf(pubkey)` su llave de cifrado, para sellarle la respuesta
|
|
30
|
-
* `needsApproval(pubkey)` si ese aparato
|
|
30
|
+
* `needsApproval(pubkey)` (async) si ese aparato tiene que pedir aprobación según el
|
|
31
|
+
* ACTA (`unattended`). Se compone con el
|
|
31
32
|
* criterio del propio protocolo: solo lo PRIVADO pregunta
|
|
32
33
|
* `approve({ pubkey, op })` pide el visto bueno (el teléfono) y espera
|
|
33
34
|
* `devices()` / `unlink(pubkey)` administración, opcional
|
|
@@ -66,7 +67,11 @@ export function createPasswordDesk (opts = {}) {
|
|
|
66
67
|
// reescribirlo aquí: si cada bóveda tuviera su idea de qué es privado, serían tres
|
|
67
68
|
// bóvedas distintas otra vez.
|
|
68
69
|
needsApproval: async (op, payload, pubkey) =>
|
|
69
|
-
needsApproval
|
|
70
|
+
// `await` OBLIGATORIO: `needsApproval` pasó a ser asíncrona (lee el acta en vivo), y
|
|
71
|
+
// una promesa SIEMPRE es verdadera. Sin el await esto decía «sí, pide aprobación»
|
|
72
|
+
// para todo el mundo, y en el otro sentido —si algún día devuelve false— diría que
|
|
73
|
+
// no hace falta. Un `&&` sobre una promesa es una comprobación que no comprueba.
|
|
74
|
+
await needsApproval(pubkey) && await responder.wantsPrivate(op, payload),
|
|
70
75
|
approve,
|
|
71
76
|
admin: devices && unlink ? { devices, unlink } : null,
|
|
72
77
|
onRequest: (r) => {
|
package/src/tui/app.js
CHANGED
|
@@ -332,8 +332,13 @@ function pairModeRows (st, t) {
|
|
|
332
332
|
* `sealer` es justo el que hay que entender para el multivault. Se ven siempre, aunque en
|
|
333
333
|
* un teléfono normal `sealer` no lo vaya a usar nadie: verlo apagado explica el modelo
|
|
334
334
|
* (dueño, 2026-08-31).
|
|
335
|
+
*
|
|
336
|
+
* `unattended` entró por lo mismo (dueño, 2026-09-01): era una marca local de la bóveda
|
|
337
|
+
* —invisible aquí y solo tocable por la CLI— y encima con el sentido invertido. Es EL
|
|
338
|
+
* permiso que hay que entender para un servidor: dice si esa máquina se lleva tus claves
|
|
339
|
+
* privadas sola o si te lo pregunta. Verlo apagado es la mitad del mensaje.
|
|
335
340
|
*/
|
|
336
|
-
const CAPS_ORDER = ['sign', 'store', 'read', 'admin', 'approve', 'passwords', 'sealer']
|
|
341
|
+
const CAPS_ORDER = ['sign', 'store', 'read', 'admin', 'approve', 'passwords', 'sealer', 'unattended']
|
|
337
342
|
|
|
338
343
|
function capsRows (st, t) {
|
|
339
344
|
const i = L(st)
|
|
@@ -342,20 +347,31 @@ function capsRows (st, t) {
|
|
|
342
347
|
const member = (st.members || []).find((m) => m.pub === target.pub)
|
|
343
348
|
if (!member) return [{ text: t.muted(i.capsNoMember), sel: false }]
|
|
344
349
|
|
|
345
|
-
|
|
350
|
+
// EL BORRADOR manda sobre lo que hay en el acta: es lo que estás a punto de dejar.
|
|
351
|
+
const real = new Set(member.caps || [])
|
|
352
|
+
const has = st.capsDraft?.pub === member.pub ? new Set(st.capsDraft.caps) : real
|
|
353
|
+
|
|
346
354
|
const rows = [
|
|
347
355
|
{ text: ' ' + t.bold(i.capsFor(target.deviceId, member.label || '')), sel: false },
|
|
348
356
|
{ text: '', sel: false }
|
|
349
357
|
]
|
|
358
|
+
let tocados = 0
|
|
350
359
|
for (const cap of CAPS_ORDER) {
|
|
351
360
|
const mark = has.has(cap) ? '[x]' : '[ ]'
|
|
352
361
|
const name = i.capName[cap]
|
|
353
|
-
|
|
362
|
+
// LO QUE CAMBIA SE SEÑALA. Sin esto el borrador es indistinguible del acta y no sabes
|
|
363
|
+
// qué vas a firmar — que es justo lo que hace peligroso acumular cambios.
|
|
364
|
+
const cambia = has.has(cap) !== real.has(cap)
|
|
365
|
+
if (cambia) tocados++
|
|
366
|
+
const line = ` ${mark}${cambia ? t.bold('*') : ' '} ${cap === 'admin' ? t.bold(name) : name}`
|
|
354
367
|
rows.push({ text: line, sel: true, meta: { cap } })
|
|
355
368
|
rows.push({ text: t.muted(' ' + i.capHint[cap]), sel: false })
|
|
356
369
|
}
|
|
357
370
|
rows.push({ text: '', sel: false })
|
|
358
|
-
rows.push({
|
|
371
|
+
rows.push({
|
|
372
|
+
text: tocados ? ' ' + t.bold(i.capsPending(tocados)) : t.muted(' ' + i.capsApplyHint),
|
|
373
|
+
sel: false
|
|
374
|
+
})
|
|
359
375
|
return rows
|
|
360
376
|
}
|
|
361
377
|
|
|
@@ -924,6 +940,8 @@ async function onKeyDevices (term, st, key) {
|
|
|
924
940
|
})
|
|
925
941
|
} else if (ch === 'c' && cur?.sub) {
|
|
926
942
|
st.capsFor = { pub: cur.sub, deviceId: cur.deviceId }
|
|
943
|
+
// Borrador limpio al entrar: arrastrar el de otro aparato sería firmar lo que no viste.
|
|
944
|
+
st.capsDraft = null
|
|
927
945
|
st.sel.caps = 0
|
|
928
946
|
await refreshMembers(term, st)
|
|
929
947
|
st.screen = 'caps'
|
|
@@ -998,9 +1016,19 @@ async function beginPairing (term, st, profile, service = null, label = null) {
|
|
|
998
1016
|
}
|
|
999
1017
|
|
|
1000
1018
|
/**
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1019
|
+
* UN BORRADOR QUE SE GUARDA O SE DESCARTA (dueño, 2026-09-01).
|
|
1020
|
+
*
|
|
1021
|
+
* Antes cada Enter sellaba un acta. Cambiar cuatro permisos eran cuatro actas, cuatro
|
|
1022
|
+
* avisos a todos los aparatos y cuatro renovaciones de certificado — absurdo, y encima
|
|
1023
|
+
* dejaba la cuenta pasando por estados intermedios que nadie quiso (un aparato con
|
|
1024
|
+
* `admin` pero todavía sin `read`, por ejemplo).
|
|
1025
|
+
*
|
|
1026
|
+
* Ahora Enter mueve un borrador LOCAL —nada firmado, nada avisado— y `G` lo aplica de
|
|
1027
|
+
* golpe. `applyChanges` ya tomaba una LISTA de cambios y producía UNA acta: el borrador es
|
|
1028
|
+
* esa lista, sin firmar, hasta que confirmas.
|
|
1029
|
+
*
|
|
1030
|
+
* Lo que se cambia va marcado con `*` y contado abajo: un borrador que no se distingue del
|
|
1031
|
+
* acta es peor que no tenerlo, porque firmas sin saber qué.
|
|
1004
1032
|
*/
|
|
1005
1033
|
async function onKeyCaps (term, st, key) {
|
|
1006
1034
|
const i = L(st)
|
|
@@ -1010,31 +1038,51 @@ async function onKeyCaps (term, st, key) {
|
|
|
1010
1038
|
const cur = sels[Math.min(st.sel.caps || 0, sels.length - 1)]
|
|
1011
1039
|
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
1012
1040
|
|
|
1013
|
-
if (key.name === 'escape' || ch === 'b') { st.screen = 'devices'; st.capsFor = null; return true }
|
|
1014
|
-
if (key.name === 'f5') { await refreshMembers(term, st); return true }
|
|
1015
|
-
if ((key.name !== 'enter' && ch !== ' ') || !cur) return true
|
|
1016
|
-
|
|
1017
1041
|
const member = (st.members || []).find((m) => m.pub === st.capsFor?.pub)
|
|
1018
|
-
if (!member)
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (giving) caps.add(cur.cap); else caps.delete(cur.cap)
|
|
1022
|
-
|
|
1023
|
-
const apply = async () => {
|
|
1024
|
-
const r = await guard(term, st, i.applyingCaps, () => vc.setDeviceCaps(member.pub, [...caps], activeId(st)))
|
|
1025
|
-
if (!r.ok) return
|
|
1026
|
-
applyDump(st, r.v)
|
|
1027
|
-
await refreshMembers(term, st)
|
|
1028
|
-
flash(st, giving ? i.capGiven(i.capName[cur.cap]) : i.capTaken(i.capName[cur.cap]))
|
|
1042
|
+
if (!member) {
|
|
1043
|
+
if (key.name === 'escape' || ch === 'b') { st.screen = 'devices'; st.capsFor = null }
|
|
1044
|
+
return true
|
|
1029
1045
|
}
|
|
1046
|
+
const real = new Set(member.caps || [])
|
|
1047
|
+
if (st.capsDraft?.pub !== member.pub) st.capsDraft = { pub: member.pub, caps: [...real] }
|
|
1048
|
+
const draft = new Set(st.capsDraft.caps)
|
|
1049
|
+
const sucio = [...new Set([...draft, ...real])].some((c) => draft.has(c) !== real.has(c))
|
|
1030
1050
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1051
|
+
const salir = () => { st.screen = 'devices'; st.capsFor = null; st.capsDraft = null }
|
|
1052
|
+
if (key.name === 'escape' || ch === 'b') {
|
|
1053
|
+
// SALIR CON CAMBIOS SIN GUARDAR SE PREGUNTA. Tirarlos callando es perder trabajo sin
|
|
1054
|
+
// decirlo, y es justo lo que un borrador no debe hacer.
|
|
1055
|
+
if (sucio) setConfirm(st, { text: i.capsDiscard, onYes: async () => { salir() } })
|
|
1056
|
+
else salir()
|
|
1035
1057
|
return true
|
|
1036
1058
|
}
|
|
1037
|
-
await
|
|
1059
|
+
if (key.name === 'f5') { st.capsDraft = null; await refreshMembers(term, st); return true }
|
|
1060
|
+
|
|
1061
|
+
// GUARDAR: UNA sola acta con todo lo que hayas tocado.
|
|
1062
|
+
if (ch === 'g') {
|
|
1063
|
+
if (!sucio) { flash(st, i.capsNothing); return true }
|
|
1064
|
+
const cuantos = [...new Set([...draft, ...real])].filter((c) => draft.has(c) !== real.has(c)).length
|
|
1065
|
+
const guardar = async () => {
|
|
1066
|
+
const r = await guard(term, st, i.applyingCaps, () => vc.setDeviceCaps(member.pub, [...draft], activeId(st)))
|
|
1067
|
+
if (!r.ok) return
|
|
1068
|
+
applyDump(st, r.v)
|
|
1069
|
+
st.capsDraft = null
|
|
1070
|
+
await refreshMembers(term, st)
|
|
1071
|
+
flash(st, i.capsSaved(cuantos))
|
|
1072
|
+
}
|
|
1073
|
+
// Administrar se PREGUNTA, y se pregunta AL GUARDAR: es cuando pasa a ser verdad.
|
|
1074
|
+
if (draft.has('admin') && !real.has('admin')) {
|
|
1075
|
+
setConfirm(st, { text: i.confirmAdmin(st.capsFor.deviceId), onYes: guardar })
|
|
1076
|
+
return true
|
|
1077
|
+
}
|
|
1078
|
+
await guardar()
|
|
1079
|
+
return true
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if ((key.name !== 'enter' && ch !== ' ') || !cur) return true
|
|
1083
|
+
// Enter solo mueve el BORRADOR: no firma, no avisa, no se puede equivocar caro.
|
|
1084
|
+
if (draft.has(cur.cap)) draft.delete(cur.cap); else draft.add(cur.cap)
|
|
1085
|
+
st.capsDraft = { pub: member.pub, caps: [...draft] }
|
|
1038
1086
|
return true
|
|
1039
1087
|
}
|
|
1040
1088
|
|
|
@@ -1814,4 +1862,4 @@ export async function runTui () {
|
|
|
1814
1862
|
}
|
|
1815
1863
|
|
|
1816
1864
|
// Solo para pruebas headless (render sin terminal real). No usar en runtime.
|
|
1817
|
-
export const __test = { render, activeLocked, autoLockedIds, autoLockWakeIn, forgetAutoLocked, autoLockMin, refreshAll, ensureUnlocked, profileRows, deviceRows, secretRows, devVarRows, meRows, capsRows, pairModeRows, pairingBody, scrollBody, fitHelp, toggleLang, mergeMembersAndCerts, seguirAqui, resetToque: () => { ultimoToque = 0 } }
|
|
1865
|
+
export const __test = { render, activeLocked, autoLockedIds, autoLockWakeIn, forgetAutoLocked, autoLockMin, refreshAll, ensureUnlocked, profileRows, deviceRows, secretRows, devVarRows, meRows, capsRows, onKeyCaps, pairModeRows, pairingBody, scrollBody, fitHelp, toggleLang, mergeMembersAndCerts, seguirAqui, resetToque: () => { ultimoToque = 0 } }
|
package/src/tui/i18n.js
CHANGED
|
@@ -40,7 +40,11 @@ const es = {
|
|
|
40
40
|
titleCaps: 'Permisos del dispositivo',
|
|
41
41
|
capsFor: (id, name) => `Permisos de ${id}${name ? ' · ' + name : ''}`,
|
|
42
42
|
capsNoMember: ' (este dispositivo ya no está en el acta)',
|
|
43
|
-
capsApplyHint: 'Enter marca o desmarca
|
|
43
|
+
capsApplyHint: 'Enter marca o desmarca · G guarda los cambios · Esc sale',
|
|
44
|
+
capsPending: (n) => `${n} cambio(s) sin guardar (*) · G para guardarlos en UNA sola acta · Esc para descartarlos`,
|
|
45
|
+
capsSaved: (n) => `${n} cambio(s) guardados en una sola acta`,
|
|
46
|
+
capsNothing: 'No has cambiado nada',
|
|
47
|
+
capsDiscard: '¿Descartar los cambios sin guardar?',
|
|
44
48
|
capName: {
|
|
45
49
|
sign: 'Firmar como tú',
|
|
46
50
|
store: 'Guardar tus datos',
|
|
@@ -48,7 +52,8 @@ const es = {
|
|
|
48
52
|
admin: 'Administrar el perfil',
|
|
49
53
|
passwords: 'Pedir tus contraseñas',
|
|
50
54
|
approve: 'Aprobar pedidos',
|
|
51
|
-
sealer: 'Sellar el acta'
|
|
55
|
+
sealer: 'Sellar el acta',
|
|
56
|
+
unattended: 'Recibir claves sin aprobación'
|
|
52
57
|
},
|
|
53
58
|
capHint: {
|
|
54
59
|
sign: 'usar tu identidad en las apps del ecosistema',
|
|
@@ -61,14 +66,15 @@ const es = {
|
|
|
61
66
|
// daemon, o un navegador que se puso de bóveda). En un teléfono normal se concede y no
|
|
62
67
|
// lo usa nadie. Se enseña igual, y se dice: verlo desactivado explica el modelo;
|
|
63
68
|
// esconderlo hacía creer que no existe (dueño, 2026-08-31).
|
|
64
|
-
sealer: 'admitir aparatos y cambiar permisos él solo, si esta bóveda no está. Solo sirve en otra BÓVEDA'
|
|
69
|
+
sealer: 'admitir aparatos y cambiar permisos él solo, si esta bóveda no está. Solo sirve en otra BÓVEDA',
|
|
70
|
+
unattended: 'llevarse claves privadas SIN preguntarte. Apagado, cada vez que arranque tienes que aprobarlo desde un aparato con «Aprobar»'
|
|
65
71
|
},
|
|
66
72
|
confirmAdmin: (id) => `¿Dejar que ${id} conecte y quite dispositivos sin venir aquí?`,
|
|
67
73
|
capGiven: (n) => `Concedido: ${n}`,
|
|
68
74
|
capTaken: (n) => `Quitado: ${n}`,
|
|
69
75
|
applyingCaps: 'Aplicando…',
|
|
70
76
|
loadingMembers: 'Cargando el acta…',
|
|
71
|
-
helpCaps: ['↑↓', 'Enter marcar', 'F5 refrescar', 'Esc dispositivos', 'l English', 'q salir'],
|
|
77
|
+
helpCaps: ['↑↓', 'Enter marcar', 'G guardar', 'F5 refrescar', 'Esc dispositivos', 'l English', 'q salir'],
|
|
72
78
|
// Perfil del usuario (lo que sincronizan los dispositivos). Solo lectura: se edita en
|
|
73
79
|
// el aparato, no en la máquina donde vive la bóveda.
|
|
74
80
|
loadingProfile: 'Cargando el perfil…',
|
|
@@ -350,7 +356,11 @@ const en = {
|
|
|
350
356
|
titleCaps: 'Device permissions',
|
|
351
357
|
capsFor: (id, name) => `Permissions for ${id}${name ? ' · ' + name : ''}`,
|
|
352
358
|
capsNoMember: ' (this device is no longer in the record)',
|
|
353
|
-
capsApplyHint: 'Enter ticks or unticks
|
|
359
|
+
capsApplyHint: 'Enter ticks or unticks · G saves · Esc leaves',
|
|
360
|
+
capsPending: (n) => `${n} unsaved change(s) (*) · G to save them in ONE record · Esc to discard`,
|
|
361
|
+
capsSaved: (n) => `${n} change(s) saved in a single record`,
|
|
362
|
+
capsNothing: 'You have not changed anything',
|
|
363
|
+
capsDiscard: 'Discard the unsaved changes?',
|
|
354
364
|
capName: {
|
|
355
365
|
sign: 'Sign as you',
|
|
356
366
|
store: 'Save your data',
|
|
@@ -358,7 +368,8 @@ const en = {
|
|
|
358
368
|
admin: 'Manage the profile',
|
|
359
369
|
passwords: 'Ask for your passwords',
|
|
360
370
|
approve: 'Approve requests',
|
|
361
|
-
sealer: 'Seal the record'
|
|
371
|
+
sealer: 'Seal the record',
|
|
372
|
+
unattended: 'Take keys without approval'
|
|
362
373
|
},
|
|
363
374
|
capHint: {
|
|
364
375
|
sign: 'use your identity across the ecosystem apps',
|
|
@@ -367,14 +378,15 @@ const en = {
|
|
|
367
378
|
admin: 'connect and remove devices from that device, without coming here',
|
|
368
379
|
passwords: 'the manager: ask for one credential per site, never the whole list',
|
|
369
380
|
approve: 'give the go-ahead when another device asks for private keys',
|
|
370
|
-
sealer: 'admit devices and change permissions on its own if this vault is gone. Only useful on another VAULT'
|
|
381
|
+
sealer: 'admit devices and change permissions on its own if this vault is gone. Only useful on another VAULT',
|
|
382
|
+
unattended: 'take private keys WITHOUT asking you. Off, you must approve it from a device with «Approve» every time it starts'
|
|
371
383
|
},
|
|
372
384
|
confirmAdmin: (id) => `Let ${id} connect and remove devices without coming here?`,
|
|
373
385
|
capGiven: (n) => `Granted: ${n}`,
|
|
374
386
|
capTaken: (n) => `Removed: ${n}`,
|
|
375
387
|
applyingCaps: 'Applying…',
|
|
376
388
|
loadingMembers: 'Loading the record…',
|
|
377
|
-
helpCaps: ['↑↓', 'Enter tick', 'F5 refresh', 'Esc devices', 'l Español', 'q quit'],
|
|
389
|
+
helpCaps: ['↑↓', 'Enter tick', 'G save', 'F5 refresh', 'Esc devices', 'l Español', 'q quit'],
|
|
378
390
|
loadingProfile: 'Loading profile…',
|
|
379
391
|
noProfile: ' (this vault has no profile yet)',
|
|
380
392
|
noProfileHint: ' Edit your name or photo on a paired device and press F5.',
|
package/src/vault.js
CHANGED
|
@@ -113,18 +113,25 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
113
113
|
* dos clases de espera.
|
|
114
114
|
*/
|
|
115
115
|
const waiters = new Map()
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
116
|
+
/**
|
|
117
|
+
* ¿ESTE APARATO TIENE QUE PEDIR PERMISO PARA RECIBIR CLAVES PRIVADAS?
|
|
118
|
+
*
|
|
119
|
+
* Lo dice el ACTA, con el permiso `unattended`: quien lo tiene se las lleva solo; quien
|
|
120
|
+
* no, espera a que un aparato con `approve` lo firme.
|
|
121
|
+
*
|
|
122
|
+
* Antes era al revés y vivía aquí: una lista local de «estos SÍ piden permiso», así que
|
|
123
|
+
* un aparato nuevo nacía pudiendo llevarse las claves y nadie elegía eso — se elegía por
|
|
124
|
+
* omisión. Ahora hay que conceder a propósito quién puede llevárselas solo, y **si falta
|
|
125
|
+
* el dato se pide permiso**, que es lo que hay que hacer cuando no se sabe (dueño,
|
|
126
|
+
* 2026-09-01).
|
|
127
|
+
*
|
|
128
|
+
* Y al estar en el acta lo respeta cualquier bóveda de la cuenta, se ve en la pantalla de
|
|
129
|
+
* permisos como los demás, y se quita quitándolo — sin acordarse de un registro escondido
|
|
130
|
+
* en una máquina.
|
|
131
|
+
*/
|
|
132
|
+
function needsApproval (pub, record) {
|
|
133
|
+
if (!record) return true // sin acta no se decide que sí: se pide permiso
|
|
134
|
+
return !Acta.memberCan(record, pub, 'unattended')
|
|
128
135
|
}
|
|
129
136
|
// LA BÓVEDA DE CONTRASEÑAS: entradas y su llave, en el dir del perfil y cifradas en
|
|
130
137
|
// reposo como todo lo demás. QUIÉN PUEDE PEDIR LO DICE EL ACTA (capacidad
|
|
@@ -835,7 +842,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
835
842
|
// claves en memoria y no vuelve a pedir. El pedido se apunta, se avisa a quien aprueba y se
|
|
836
843
|
// contesta «pendiente»; la respuesta de verdad sale cuando el teléfono firme
|
|
837
844
|
// (`handleApproval`), sellada a la misma `ek`.
|
|
838
|
-
if (needsApproval(chk.device)) {
|
|
845
|
+
if (needsApproval(chk.device, record)) {
|
|
839
846
|
const deviceId = await deviceIdOf(chk.device).catch(() => null)
|
|
840
847
|
const label = (record?.members || []).find((m) => m.pub === chk.device)?.label || ''
|
|
841
848
|
const pend = approvals.request({ ns, device: chk.device, deviceId, label, ek: p.data.ek })
|
|
@@ -1057,7 +1064,11 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1057
1064
|
// que haya una segunda lista que acordarse de tocar.
|
|
1058
1065
|
isAllowed: (pub) => !!actaCache && Acta.memberCan(actaCache, pub, 'passwords'),
|
|
1059
1066
|
encPubOf: (pub) => (actaCache?.members || []).find((m) => m.pub === pub)?.encPub || null,
|
|
1060
|
-
|
|
1067
|
+
// EN VIVO, no del caché: conceder o quitar `unattended` tiene que surtir efecto en
|
|
1068
|
+
// el acto. Con el caché, la mesa de contraseñas seguía decidiendo con la foto del
|
|
1069
|
+
// último refresco — y en el sentido peligroso: un aparato al que le acabas de quitar
|
|
1070
|
+
// el permiso se seguía sirviendo solo.
|
|
1071
|
+
needsApproval: async (pub) => needsApproval(pub, await refreshActa()),
|
|
1061
1072
|
// El teléfono: se apunta el pedido, se le avisa y esta promesa espera su firma.
|
|
1062
1073
|
// Es el mismo camino que ya recorren los cajones de secretos.
|
|
1063
1074
|
approve: async ({ pubkey, op }) => {
|
|
@@ -2076,11 +2087,10 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2076
2087
|
startPairing: desk.startPairing,
|
|
2077
2088
|
stopPairing: desk.stopPairing,
|
|
2078
2089
|
listPending: desk.listPending,
|
|
2079
|
-
//
|
|
2080
|
-
//
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
supervised: () => readSupervised(),
|
|
2090
|
+
// ¿Pide permiso este aparato para recibir claves privadas? Lo dice el ACTA
|
|
2091
|
+
// (`unattended`), no una lista de esta máquina — por eso ya no hay `setApproval` ni
|
|
2092
|
+
// `supervised`: se concede y se quita como cualquier otro permiso, con `caps`.
|
|
2093
|
+
needsApproval: async (pub) => needsApproval(pub, await refreshActa()),
|
|
2084
2094
|
// La bóveda de contraseñas (`passwords.js`). Aquí SÍ se lista: es donde está la
|
|
2085
2095
|
// llave. Lo que no puede es listarla un aparato.
|
|
2086
2096
|
passwordDevices,
|