@dotrino/vaultd 0.38.0 → 0.46.2
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/README.md +4 -1
- package/lib/src/admin.js +4 -0
- package/lib/src/atrest.js +0 -0
- package/lib/src/enroll.js +12 -3
- package/lib/src/protocol.js +10 -0
- package/lib/src/service.js +288 -52
- package/package.json +6 -6
- package/src/ctl.js +273 -17
- package/src/daemon.js +161 -14
- package/src/manager.js +6 -0
- package/src/profiles.js +82 -9
- package/src/sealKey.js +80 -0
- package/src/sealer.js +170 -0
- package/src/secretsStore.js +777 -91
- package/src/store.js +3 -1
- package/src/tui/app.js +119 -9
- package/src/tui/i18n.js +31 -6
- package/src/vault.js +647 -30
- package/src/vaultControl.js +50 -14
package/src/ctl.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import fs from 'node:fs'
|
|
24
24
|
import path from 'node:path'
|
|
25
25
|
import { execFileSync } from 'node:child_process'
|
|
26
|
+
import tty from 'node:tty'
|
|
26
27
|
import { pubkeyId } from '@dotrino/identity/capabilities'
|
|
27
28
|
import { dataDir, readJson } from './paths.js'
|
|
28
29
|
import { assertVar } from './secretsStore.js'
|
|
@@ -179,6 +180,30 @@ async function cmdPair (args = []) {
|
|
|
179
180
|
console.error('uso: dotrino-vault pair --service <ns> (ns en minúsculas, p.ej. proxy)'); process.exit(2)
|
|
180
181
|
}
|
|
181
182
|
}
|
|
183
|
+
// --scope <lista>: los PERMISOS del cert, y nada más. No hay tipos de aparato
|
|
184
|
+
// (2026-08-22, dueño): un aparato es un aparato y lo que puede hacer. Sin --scope,
|
|
185
|
+
// el juego de siempre (sign,read,store); `--service <ns>` es el atajo de
|
|
186
|
+
// `secrets:<ns>`, y los dos se combinan (`--service eco --scope sign` = un bot que
|
|
187
|
+
// firma como aparato del acta y lee solo su cajón). `admin` no se empareja: se
|
|
188
|
+
// concede desde el PC (`caps <ID> +administra`).
|
|
189
|
+
const scIdx = args.indexOf('--scope')
|
|
190
|
+
let scope = null
|
|
191
|
+
if (scIdx >= 0) {
|
|
192
|
+
const raw = args[scIdx + 1]
|
|
193
|
+
if (!raw || raw.startsWith('-')) { console.error('uso: dotrino-vault pair --scope sign,read,store,secrets:<ns>'); process.exit(2) }
|
|
194
|
+
const ALIAS = { firma: 'sign', lee: 'read', guarda: 'store' }
|
|
195
|
+
scope = []
|
|
196
|
+
for (const tok of raw.split(',').map((t) => t.trim()).filter(Boolean)) {
|
|
197
|
+
const t = ALIAS[tok] || tok
|
|
198
|
+
if (t === 'admin' || t === 'administra') { console.error('`admin` no se empareja: concédelo desde el PC con dotrino-vault caps <ID> +administra'); process.exit(2) }
|
|
199
|
+
if (t === 'sign' || t === 'read' || t === 'store') { scope.push('vault:' + t); continue }
|
|
200
|
+
const m = /^secrets:([a-z0-9-]{1,32})$/.exec(t)
|
|
201
|
+
if (m) { scope.push('vault:secrets:' + m[1]); continue }
|
|
202
|
+
console.error('permiso desconocido: %s (sign | read | store | secrets:<ns>)', tok); process.exit(2)
|
|
203
|
+
}
|
|
204
|
+
if (service) scope.push('vault:secrets:' + service)
|
|
205
|
+
scope = [...new Set(scope)]
|
|
206
|
+
}
|
|
182
207
|
// `--new-account [nombre]`: la otra respuesta a «¿a qué cuenta entra?». En vez de
|
|
183
208
|
// meter el dispositivo en una cuenta que ya vive aquí, se ESTRENA una (vacía) y
|
|
184
209
|
// entra a ella; las demás no se tocan. En la TUI esto es una pregunta con sus
|
|
@@ -210,7 +235,7 @@ async function cmdPair (args = []) {
|
|
|
210
235
|
}
|
|
211
236
|
// La petición se escribe SIEMPRE (aunque no haya --service): lleva a qué perfil
|
|
212
237
|
// se empareja el dispositivo.
|
|
213
|
-
writeReq('pair-request.json', { ...(service ? { service } : {}), ...(adopt ? { mode: 'adopt' } : {}) })
|
|
238
|
+
writeReq('pair-request.json', { ...(service ? { service } : {}), ...(scope ? { scope } : {}), ...(adopt ? { mode: 'adopt' } : {}) })
|
|
214
239
|
sendSignal(s.pid, 'SIGUSR1')
|
|
215
240
|
|
|
216
241
|
let pair = null
|
|
@@ -369,6 +394,10 @@ async function cmdMembers () {
|
|
|
369
394
|
].filter(Boolean)
|
|
370
395
|
const caps = m.caps.length ? m.caps.map((c) => CAP[c] || c).join(', ') : '(sin permisos)'
|
|
371
396
|
console.log(' %s %s%s\n %s', m.id, who, marks.length ? ' [' + marks.join(' · ') + ']' : '', caps)
|
|
397
|
+
// Un servicio SIN llave de cifrado no puede leer ninguna variable privada: van
|
|
398
|
+
// selladas a esa llave. Se dice aquí, junto a él, porque es el único sitio donde
|
|
399
|
+
// se mira quién es quién — y en la lista de variables ya seria tarde.
|
|
400
|
+
if (m.cn && !m.canSeal) console.log(' %ssin llave de cifrado: NO puede leer sus variables%s', R, Z)
|
|
372
401
|
}
|
|
373
402
|
console.log('\n Cambiar permisos: dotrino-vault caps <ID> +firma | -firma | +guarda | -guarda | +lee | -lee | +administra')
|
|
374
403
|
console.log(' «Administra» deja conectar y quitar dispositivos desde ese aparato, sin venir aquí.')
|
|
@@ -601,6 +630,37 @@ async function cmdSecret (rest) {
|
|
|
601
630
|
})
|
|
602
631
|
const [sub, ...args] = rest2
|
|
603
632
|
const s = requireDaemon()
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* La contraseña del perfil, y SOLO si el perfil la tiene.
|
|
636
|
+
*
|
|
637
|
+
* Desde v5 la piden únicamente las operaciones que **LEEN** un valor: verlo, cambiarle
|
|
638
|
+
* la visibilidad (para enseñarlo hay que sacarlo), convertir el archivo y saldar lo que
|
|
639
|
+
* quedó a deber. **Guardar no la pide**: sellar solo necesita las públicas de quien va
|
|
640
|
+
* a leer (`docs/secretos-sellados.md` §8.1). Borrar tampoco, porque tirar un sobre no
|
|
641
|
+
* obliga a abrirlo.
|
|
642
|
+
*/
|
|
643
|
+
let cachedPwd
|
|
644
|
+
const adminPassword = async () => {
|
|
645
|
+
if (cachedPwd !== undefined) return cachedPwd
|
|
646
|
+
const d = await profileRequest('list')
|
|
647
|
+
const list = Array.isArray(d.profiles) ? d.profiles : []
|
|
648
|
+
const p = PROFILE
|
|
649
|
+
? list.find((x) => x.id === PROFILE || x.name === PROFILE)
|
|
650
|
+
: (list.find((x) => x.current) || list[0])
|
|
651
|
+
if (!p?.protected) { cachedPwd = null; return null }
|
|
652
|
+
// Nunca por argumento: quedaría en `ps` y en el historial de la shell. Si no hay
|
|
653
|
+
// terminal se dice qué falta, en vez de fallar con «el daemon no aplicó el cambio».
|
|
654
|
+
let pwd
|
|
655
|
+
try { pwd = await askPassword('Contraseña del perfil: ') } catch (_) {
|
|
656
|
+
console.error('Este perfil tiene contraseña y hace falta para guardar una variable.')
|
|
657
|
+
console.error('Ejecútalo desde un terminal (por ssh, con -t).')
|
|
658
|
+
process.exit(1)
|
|
659
|
+
}
|
|
660
|
+
if (!pwd) { console.error('Cancelado.'); process.exit(1) }
|
|
661
|
+
cachedPwd = pwd
|
|
662
|
+
return pwd
|
|
663
|
+
}
|
|
604
664
|
const secretsListFile = path.join(dir, 'secrets-list.json')
|
|
605
665
|
const signalAndWaitList = async () => {
|
|
606
666
|
try { fs.rmSync(secretsListFile, { force: true }) } catch (_) {}
|
|
@@ -615,6 +675,26 @@ async function cmdSecret (rest) {
|
|
|
615
675
|
}
|
|
616
676
|
console.error('El daemon no respondió.'); process.exit(1)
|
|
617
677
|
}
|
|
678
|
+
/**
|
|
679
|
+
* SELLA el almacén: pasa un `secrets.json` v3 (valores que esta bóveda puede leer) a
|
|
680
|
+
* v4 (cada privada cifrada a la llave de su destinatario). Corre UNA vez.
|
|
681
|
+
*
|
|
682
|
+
* Es una operación con nombre propio y no un efecto de desbloquear, porque es el
|
|
683
|
+
* punto de no retorno: a partir de aquí la contraseña es lo ÚNICO que puede
|
|
684
|
+
* re-sellar, y perderla impide rotar una variable o sumar un aparato al namespace.
|
|
685
|
+
* Deja `secrets.json.v3.bak` al lado para poder volver.
|
|
686
|
+
*/
|
|
687
|
+
async function secretMigrate () {
|
|
688
|
+
console.log('Esto SELLA las variables privadas a la llave de cada aparato.')
|
|
689
|
+
console.log('A partir de aquí, esta contraseña es lo único que puede volver a sellarlas:')
|
|
690
|
+
console.log('si la pierdes no podrás rotar una variable ni sumar un aparato al namespace.')
|
|
691
|
+
console.log('Se deja una copia en secrets.json.v3.bak por si hay que volver.\n')
|
|
692
|
+
const pwd = await askPassword('Contraseña del perfil: ')
|
|
693
|
+
if (!pwd) { console.error('Cancelado.'); process.exit(1) }
|
|
694
|
+
writeReq('secret-request.json', { op: 'migrate', password: pwd })
|
|
695
|
+
console.log('\nPedido enviado. Mira el resultado con: journalctl --user -u dotrino-vault -n 20')
|
|
696
|
+
}
|
|
697
|
+
|
|
618
698
|
const USAGE = [
|
|
619
699
|
'uso: dotrino-vault secret set <ns> <CLAVE> <valor> [--public|--private]',
|
|
620
700
|
' (la comparten todos los aparatos del ns)',
|
|
@@ -627,6 +707,7 @@ async function cmdSecret (rest) {
|
|
|
627
707
|
' dotrino-vault secret device import <ID> [archivo.env]',
|
|
628
708
|
' dotrino-vault secret device rm <ID> <CLAVE>',
|
|
629
709
|
' dotrino-vault secret list',
|
|
710
|
+
' dotrino-vault secret migrate SELLA los secretos (una vez)',
|
|
630
711
|
'',
|
|
631
712
|
'CARGA LA CONFIGURACIÓN DE UN SERVICIO DE UNA VEZ (`set` con varios pares, o `import`):',
|
|
632
713
|
'la bóveda la aplica entera y avisa UNA sola vez. De una en una, cada variable es un',
|
|
@@ -651,13 +732,16 @@ async function cmdSecret (rest) {
|
|
|
651
732
|
process.exit(2)
|
|
652
733
|
}
|
|
653
734
|
const withVisibility = items.map((it) => (isPublic === undefined ? it : { ...it, public: isPublic }))
|
|
654
|
-
|
|
735
|
+
// Guardar NO pide la contraseña (§8.1).
|
|
736
|
+
const base = pub ? { op: 'batch', pub, items: withVisibility } : { op: 'batch', ns, items: withVisibility }
|
|
737
|
+
writeReq('secret-request.json', base)
|
|
655
738
|
const d = await signalAndWaitList()
|
|
656
739
|
const list = pub
|
|
657
740
|
? ((Array.isArray(d.dev) ? d.dev : []).find((x) => x.pub === pub)?.keys || [])
|
|
658
741
|
: (d.ns?.[ns] || [])
|
|
659
742
|
const missing = items.filter((it) => !has(list, it.key)).map((it) => it.key)
|
|
660
743
|
if (missing.length) {
|
|
744
|
+
if (d?.secretError) return noSeAplico(d)
|
|
661
745
|
console.error('El daemon no guardó: %s (revisa: dotrino-vault logs)', missing.join(', '))
|
|
662
746
|
process.exit(1)
|
|
663
747
|
}
|
|
@@ -688,7 +772,126 @@ async function cmdSecret (rest) {
|
|
|
688
772
|
* dueño, era lo único que la marca no significaba). La privada no se muestra.
|
|
689
773
|
*/
|
|
690
774
|
const printVar = (k) => console.log(' · %s %s', k.key, k.public ? `${k.value ?? ''} (pública)` : '••••••')
|
|
775
|
+
/**
|
|
776
|
+
* Por qué no se aplicó. El daemon deja el motivo en el volcado siguiente; sin él lo
|
|
777
|
+
* único que se podía decir era «revisa los logs», que no ayuda a quien acaba de
|
|
778
|
+
* escribir mal la contraseña.
|
|
779
|
+
*/
|
|
780
|
+
const noSeAplico = (d) => {
|
|
781
|
+
if (d?.secretError?.code === 'WRONG_PASSWORD') console.error('Contraseña incorrecta.')
|
|
782
|
+
else if (d?.secretError?.error) console.error('No se aplicó: %s', d.secretError.error)
|
|
783
|
+
else console.error('El daemon no aplicó el cambio (revisa: dotrino-vault logs)')
|
|
784
|
+
process.exit(1)
|
|
785
|
+
}
|
|
691
786
|
const has = (list, key) => (list || []).some((x) => x.key === key)
|
|
787
|
+
/**
|
|
788
|
+
* Los cajones que quedaron a deber un sellado. Sale ARRIBA del todo en rojo porque no
|
|
789
|
+
* es un detalle: mientras esté ahí, los aparatos de ese cajón NO están leyendo sus
|
|
790
|
+
* variables, y la bóveda no puede arreglarlo sola — necesita la contraseña.
|
|
791
|
+
*/
|
|
792
|
+
/**
|
|
793
|
+
* Un perfil SIN contraseña abre sus variables privadas con la llave de esta máquina,
|
|
794
|
+
* cuyo material vive en este mismo disco: una copia del disco las abre. Es un default
|
|
795
|
+
* deliberado —tiene que seguir funcionando— pero NO es lo mismo, y callarlo es
|
|
796
|
+
* exactamente el error del comentario mentiroso de `atrest.js`.
|
|
797
|
+
*/
|
|
798
|
+
const printNoPassword = async () => {
|
|
799
|
+
const d = await profileRequest('list')
|
|
800
|
+
const list = Array.isArray(d.profiles) ? d.profiles : []
|
|
801
|
+
const p = PROFILE ? list.find((x) => x.id === PROFILE || x.name === PROFILE) : (list.find((x) => x.current) || list[0])
|
|
802
|
+
if (!p || p.protected) return
|
|
803
|
+
console.log('%sEste perfil no tiene contraseña%s: las privadas se abren con la llave de ESTA', B, Z)
|
|
804
|
+
console.log('máquina, cuyo material vive en este mismo disco — o sea que una copia del disco')
|
|
805
|
+
console.log('las abre. Ponle una con: dotrino-vault profile password\n')
|
|
806
|
+
}
|
|
807
|
+
const printPending = (d) => {
|
|
808
|
+
const pend = Object.entries(d?.pending || {})
|
|
809
|
+
if (!pend.length) return
|
|
810
|
+
console.error('%sHay %d cajón(es) sin sellar%s:', R, pend.length, Z)
|
|
811
|
+
for (const [owner, info] of pend) {
|
|
812
|
+
console.error(' · %s — %s', owner, info?.kind === 'rotate'
|
|
813
|
+
? 'se fue un miembro y no se pudo rotar su llave'
|
|
814
|
+
: 'entró un aparato y no se le pudo entregar la llave')
|
|
815
|
+
}
|
|
816
|
+
console.error('Sus aparatos NO leen sus variables. Se arregla guardando una variable')
|
|
817
|
+
console.error('de ese cajón con la contraseña: dotrino-vault secret set <ns> <CLAVE> <valor>\n')
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (sub === 'migrate') return secretMigrate()
|
|
821
|
+
|
|
822
|
+
// --- VER un valor, su histórico y volver atrás ---------------------------------
|
|
823
|
+
// Las tres van juntas porque son la misma idea: ver es lo único que la contraseña
|
|
824
|
+
// guarda, y revertir es ver + volver a guardar (§8.3/§8.4).
|
|
825
|
+
const ownerDe = async (kind, ref) => {
|
|
826
|
+
if (kind === 'ns') return `ns:${ref}`
|
|
827
|
+
const m = await findMember(ref)
|
|
828
|
+
return `dev:${m.pub}`
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (sub === 'show' || sub === 'history' || sub === 'revert') {
|
|
832
|
+
// `secret show device <ID> <CLAVE>` mira el cajón de un aparato.
|
|
833
|
+
const esDev = args[0] === 'device'
|
|
834
|
+
const [ref, key, extra] = esDev ? args.slice(1) : args
|
|
835
|
+
if (!ref || (sub !== 'history' && !key)) {
|
|
836
|
+
console.error('uso: dotrino-vault secret %s [device] <ns|ID> <CLAVE>%s', sub, sub === 'revert' ? ' <marca>' : '')
|
|
837
|
+
process.exit(2)
|
|
838
|
+
}
|
|
839
|
+
const owner = await ownerDe(esDev ? 'dev' : 'ns', ref)
|
|
840
|
+
|
|
841
|
+
if (sub === 'history') {
|
|
842
|
+
writeReq('secret-request.json', { op: 'history', owner, key: key || null })
|
|
843
|
+
const d = await signalAndWaitList()
|
|
844
|
+
const items = d.history?.items || []
|
|
845
|
+
if (!items.length) { console.log('No hay versiones anteriores de %s%s.', owner, key ? `/${key}` : ''); return }
|
|
846
|
+
console.log('\n%sVersiones anteriores%s (la de arriba es la más reciente)\n', B, Z)
|
|
847
|
+
for (const h of items) {
|
|
848
|
+
// Sin `by` no es que falte un dato: es que se escribió AQUÍ, desde esta máquina.
|
|
849
|
+
// Lo que llega por la consola remota sí trae de qué aparato vino.
|
|
850
|
+
console.log(' %s %s%s%s %s%s', new Date(h.ts).toISOString(), B, h.key, Z,
|
|
851
|
+
h.by ? `desde ${h.by.slice(0, 12)}…` : 'desde esta máquina', h.signed ? '' : ' (sobre sin firma)')
|
|
852
|
+
}
|
|
853
|
+
console.log('\nPara volver a una: dotrino-vault secret revert %s%s <CLAVE> <marca>\n',
|
|
854
|
+
esDev ? 'device ' : '', ref)
|
|
855
|
+
return
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (sub === 'revert') {
|
|
859
|
+
if (!extra) { console.error('uso: dotrino-vault secret revert [device] <ns|ID> <CLAVE> <marca>'); process.exit(2) }
|
|
860
|
+
const ts = Number.isFinite(Number(extra)) ? Number(extra) : Date.parse(extra)
|
|
861
|
+
if (!Number.isFinite(ts)) { console.error('La marca es la que enseña `secret history` (fecha ISO).'); process.exit(2) }
|
|
862
|
+
const password = await adminPassword()
|
|
863
|
+
writeReq('secret-request.json', { op: 'revert', owner, key, ts, ...(password ? { password } : {}) })
|
|
864
|
+
const d = await signalAndWaitList()
|
|
865
|
+
if (d?.secretError) return noSeAplico(d)
|
|
866
|
+
console.log('Restaurada: %s/%s', owner, key)
|
|
867
|
+
return
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// `show`: el valor. Es lo único que la contraseña guarda en esta máquina.
|
|
871
|
+
const password = await adminPassword()
|
|
872
|
+
writeReq('secret-request.json', { op: 'reveal', owner, key, ...(password ? { password } : {}) })
|
|
873
|
+
const d = await signalAndWaitList()
|
|
874
|
+
if (d?.secretError) return noSeAplico(d)
|
|
875
|
+
if (d.revealed?.owner !== owner || d.revealed?.key !== key) {
|
|
876
|
+
console.error('El daemon no devolvió el valor (revisa: dotrino-vault logs)')
|
|
877
|
+
process.exit(1)
|
|
878
|
+
}
|
|
879
|
+
if (d.revealed.value == null) { console.error('No existe esa variable.'); process.exit(1) }
|
|
880
|
+
console.log(d.revealed.value)
|
|
881
|
+
return
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Saldar lo que quedó a deber: heredarle a un aparato lo ya guardado, rotar de verdad.
|
|
885
|
+
if (sub === 'settle') {
|
|
886
|
+
const password = await adminPassword()
|
|
887
|
+
writeReq('secret-request.json', { op: 'settle', ...(password ? { password } : {}) })
|
|
888
|
+
const d = await signalAndWaitList()
|
|
889
|
+
if (d?.secretError) return noSeAplico(d)
|
|
890
|
+
const pend = Object.keys(d.pending || {})
|
|
891
|
+
if (!pend.length) console.log('Nada pendiente.')
|
|
892
|
+
else console.log('Siguen pendientes: %s', pend.join(', '))
|
|
893
|
+
return
|
|
894
|
+
}
|
|
692
895
|
|
|
693
896
|
if (sub === 'list') {
|
|
694
897
|
const d = await signalAndWaitList()
|
|
@@ -711,6 +914,8 @@ async function cmdSecret (rest) {
|
|
|
711
914
|
for (const k of x.keys) printVar(k)
|
|
712
915
|
}
|
|
713
916
|
console.log('\n(pública) = su valor se puede ver desde la consola remota. Las demás no salen de aquí.\n')
|
|
917
|
+
printPending(d)
|
|
918
|
+
await printNoPassword()
|
|
714
919
|
return
|
|
715
920
|
}
|
|
716
921
|
|
|
@@ -744,11 +949,13 @@ async function cmdSecret (rest) {
|
|
|
744
949
|
: op === 'rm'
|
|
745
950
|
? { op: 'dev-rm', pub: m.pub, key }
|
|
746
951
|
: { op: 'dev-vis', pub: m.pub, key, public: wantsPublic(value, USAGE) }
|
|
747
|
-
|
|
952
|
+
// Solo la visibilidad pide la frase: enseñar un valor obliga a sacarlo.
|
|
953
|
+
const password = op === 'vis' ? await adminPassword() : null
|
|
954
|
+
writeReq('secret-request.json', password ? { ...req, password } : req)
|
|
748
955
|
const d = await signalAndWaitList()
|
|
749
956
|
const keys = (Array.isArray(d.dev) ? d.dev : []).find((x) => x.pub === m.pub)?.keys || []
|
|
750
957
|
const ok = op === 'rm' ? !has(keys, key) : has(keys, key)
|
|
751
|
-
if (!ok)
|
|
958
|
+
if (!ok) return noSeAplico(d)
|
|
752
959
|
if (op === 'rm') console.log('Variable borrada: %s/%s', m.id, key)
|
|
753
960
|
else console.log('Variable guardada: %s/%s%s', m.id, key, (keys.find((x) => x.key === key)?.public) ? ' (pública)' : '')
|
|
754
961
|
return
|
|
@@ -782,11 +989,13 @@ async function cmdSecret (rest) {
|
|
|
782
989
|
: sub === 'rm'
|
|
783
990
|
? { op: 'rm', ns, key }
|
|
784
991
|
: { op: 'vis', ns, key, public: wantsPublic(value, USAGE) }
|
|
785
|
-
|
|
992
|
+
// Solo la visibilidad pide la frase: enseñar un valor obliga a sacarlo.
|
|
993
|
+
const password = sub === 'visibility' ? await adminPassword() : null
|
|
994
|
+
writeReq('secret-request.json', password ? { ...req, password } : req)
|
|
786
995
|
const d = await signalAndWaitList()
|
|
787
996
|
const list = d.ns?.[ns] || []
|
|
788
997
|
const ok = sub === 'rm' ? !has(list, key) : has(list, key)
|
|
789
|
-
if (!ok)
|
|
998
|
+
if (!ok) return noSeAplico(d)
|
|
790
999
|
if (sub === 'rm') console.log('Secreto borrado: %s/%s', ns, key)
|
|
791
1000
|
else console.log('Secreto guardado: %s/%s%s', ns, key, (list.find((x) => x.key === key)?.public) ? ' (pública)' : '')
|
|
792
1001
|
return
|
|
@@ -807,13 +1016,24 @@ function wantsPublic (word, usage) {
|
|
|
807
1016
|
*/
|
|
808
1017
|
function askPassword (prompt) {
|
|
809
1018
|
return new Promise((resolve, reject) => {
|
|
810
|
-
|
|
811
|
-
|
|
1019
|
+
// Se lee del TERMINAL, no de la entrada estándar. `secret import` ya usa stdin para
|
|
1020
|
+
// el `.env` (`cat .env | dotrino-vault secret import proxy`) y aun así hay que poder
|
|
1021
|
+
// escribir la contraseña; lo mismo vale para cualquier tubería.
|
|
1022
|
+
let stdin = process.stdin
|
|
1023
|
+
let own = null
|
|
1024
|
+
if (!stdin.isTTY) {
|
|
1025
|
+
try {
|
|
1026
|
+
own = fs.openSync('/dev/tty', 'r')
|
|
1027
|
+
stdin = new tty.ReadStream(own)
|
|
1028
|
+
} catch (_) { return reject(new Error('a terminal is required to type the password')) }
|
|
1029
|
+
}
|
|
812
1030
|
process.stdout.write(prompt)
|
|
813
1031
|
stdin.setRawMode(true); stdin.resume(); stdin.setEncoding('utf8')
|
|
814
1032
|
let buf = ''
|
|
815
1033
|
const done = (err, val) => {
|
|
816
|
-
stdin.setRawMode(false)
|
|
1034
|
+
try { stdin.setRawMode(false) } catch (_) {}
|
|
1035
|
+
stdin.removeListener('data', onData)
|
|
1036
|
+
if (own !== null) { try { stdin.destroy() } catch (_) {} } else stdin.pause()
|
|
817
1037
|
process.stdout.write('\n')
|
|
818
1038
|
err ? reject(err) : resolve(val)
|
|
819
1039
|
}
|
|
@@ -904,14 +1124,37 @@ async function cmdProfile (rest) {
|
|
|
904
1124
|
}
|
|
905
1125
|
case 'password': {
|
|
906
1126
|
const action = args[0]
|
|
907
|
-
|
|
1127
|
+
// QUITARLA pide la actual, y no por trámite: con ella se abre la copia maestra de
|
|
1128
|
+
// los secretos para volver a cerrarla con la llave de esta máquina. Sin ese paso,
|
|
1129
|
+
// quitar la contraseña dejaría las variables privadas ilegibles para siempre.
|
|
1130
|
+
if (action === 'rm') {
|
|
1131
|
+
console.log('Al quitarla, las variables privadas pasan a abrirse con la llave de ESTA')
|
|
1132
|
+
console.log('máquina: siguen cifradas en el disco, pero su material vive en ese mismo')
|
|
1133
|
+
console.log('disco, así que una copia del disco las abre. Los aparatos no se enteran.')
|
|
1134
|
+
const cur = await askPassword('\nContraseña actual: ')
|
|
1135
|
+
if (!cur) { console.error('Cancelado.'); process.exit(1) }
|
|
1136
|
+
reportProfiles(await profileRequest('password-rm', { password: cur }))
|
|
1137
|
+
return
|
|
1138
|
+
}
|
|
908
1139
|
if (action && action !== 'set') { console.error('uso: dotrino-vault profile password [set|rm]'); process.exit(2) }
|
|
909
|
-
console.log('La contraseña
|
|
910
|
-
console.log('funcionando (firmando
|
|
911
|
-
|
|
1140
|
+
console.log('La contraseña se pide para EDITAR el perfil y para escribir variables')
|
|
1141
|
+
console.log('privadas. Tus dispositivos siguen funcionando (firmando, leyendo y')
|
|
1142
|
+
console.log('recibiendo su configuración) aunque el perfil esté bloqueado.')
|
|
1143
|
+
// Si ya hay una, hace falta para poder abrir la copia maestra y re-sellarla. El
|
|
1144
|
+
// camino recomendado para CAMBIARLA es quitarla y ponerla: así cada paso pide
|
|
1145
|
+
// solo lo que necesita.
|
|
1146
|
+
const actual = await askPassword('\nContraseña actual (vacío si no tiene): ')
|
|
1147
|
+
// Se pide una FRASE, y se dice cómo sacarla, porque lo que da fuerza es la
|
|
1148
|
+
// longitud y que no la elija un humano: un modismo tiene la entropía de un
|
|
1149
|
+
// modismo, no la de su longitud. Desde el sellado, esta frase es lo único que
|
|
1150
|
+
// separa una copia del disco de las variables privadas.
|
|
1151
|
+
console.log('\nUsa VARIAS PALABRAS AL AZAR (mínimo 12 caracteres). Para que las elija')
|
|
1152
|
+
console.log('la máquina y no tú:')
|
|
1153
|
+
console.log(" grep -x '[a-z]\\{4,8\\}' /usr/share/dict/words | shuf -n5 | paste -sd-")
|
|
1154
|
+
const pwd = await askPassword('\nContraseña nueva: ')
|
|
912
1155
|
const again = await askPassword('Repítela: ')
|
|
913
1156
|
if (pwd !== again) { console.error('Las contraseñas no coinciden.'); process.exit(1) }
|
|
914
|
-
reportProfiles(await profileRequest('password-set', { password: pwd }))
|
|
1157
|
+
reportProfiles(await profileRequest('password-set', { password: pwd, current: actual || undefined }))
|
|
915
1158
|
return
|
|
916
1159
|
}
|
|
917
1160
|
default:
|
|
@@ -960,6 +1203,8 @@ function help () {
|
|
|
960
1203
|
estrena una cuenta VACÍA en este vault y mete ahí al dispositivo
|
|
961
1204
|
(sin la bandera entra a la cuenta activa, o a la de --profile)
|
|
962
1205
|
pair --service <ns> empareja un SERVICIO (proxy, geo…) con acceso SOLO a sus secretos
|
|
1206
|
+
pair --scope <lista> los PERMISOS del cert: sign,read,store,secrets:<ns> (sin esto: sign,read,store;
|
|
1207
|
+
se combina con --service: --service eco --scope sign = bot que firma y lee su cajón)
|
|
963
1208
|
secret set <ns> <CLAVE> <valor> variable del scope <ns>: la comparten TODOS los
|
|
964
1209
|
aparatos del perfil que sirven ese namespace
|
|
965
1210
|
secret set <ns> CLAVE=valor CLAVE2=valor2 …
|
|
@@ -975,10 +1220,21 @@ function help () {
|
|
|
975
1220
|
secret device rm <ID> <CLAVE> borra una variable de ese aparato
|
|
976
1221
|
secret list lista los dos cajones: el valor de las públicas,
|
|
977
1222
|
tapadas las privadas
|
|
1223
|
+
secret show [device] <ns|ID> <CLAVE>
|
|
1224
|
+
VE el valor de una privada. Es lo único que pide la
|
|
1225
|
+
contraseña del perfil: en esta máquina es lo único
|
|
1226
|
+
que la separa de una copia del disco
|
|
1227
|
+
secret history [device] <ns|ID> [CLAVE]
|
|
1228
|
+
las versiones anteriores (quién y cuándo)
|
|
1229
|
+
secret revert [device] <ns|ID> <CLAVE> <marca>
|
|
1230
|
+
vuelve a una versión anterior
|
|
1231
|
+
secret settle salda lo pendiente: hereda lo ya guardado a un
|
|
1232
|
+
aparato nuevo y rota de verdad el cajón del que se fue
|
|
978
1233
|
--public | --private (al hacer un set) si el VALOR puede salir de esta
|
|
979
|
-
máquina hacia la consola remota. Se nace privada
|
|
980
|
-
|
|
981
|
-
secret
|
|
1234
|
+
máquina hacia la consola remota. Se nace privada,
|
|
1235
|
+
y una privada NO se vuelve pública (bórrala y créala).
|
|
1236
|
+
secret visibility <ns> <CLAVE> private tapa una pública sin tocar el valor
|
|
1237
|
+
secret device visibility <ID> <CLAVE> private
|
|
982
1238
|
pending muestra el dispositivo pendiente + su código a comparar
|
|
983
1239
|
approve <código> aprueba el dispositivo tipeando el código que MUESTRA (el vault no lo sabe)
|
|
984
1240
|
reject <deviceId> rechaza un dispositivo pendiente
|
package/src/daemon.js
CHANGED
|
@@ -143,7 +143,19 @@ export async function runDaemon () {
|
|
|
143
143
|
if (!vault) return
|
|
144
144
|
const profileId = pairReq?.profile ? mgr.resolve(pairReq.profile) : mgr.currentId()
|
|
145
145
|
const isService = typeof pairReq?.service === 'string' && pairReq.service
|
|
146
|
-
|
|
146
|
+
// PERMISOS, no tipos (2026-08-22): el cert lleva lo que pidió `pair --scope`, y si
|
|
147
|
+
// no pidió nada, el juego de siempre. `--service <ns>` sigue siendo el atajo de
|
|
148
|
+
// `vault:secrets:<ns>`. Se valida aquí porque es la maestra la que firma: nada
|
|
149
|
+
// que no esté en esta lista entra en un cert, y `vault:admin` nunca por este camino.
|
|
150
|
+
const ALLOWED = (x) => x === 'vault:sign' || x === 'vault:read' || x === 'vault:store' || /^vault:secrets:[a-z0-9-]{1,32}$/.test(x)
|
|
151
|
+
const asked = Array.isArray(pairReq?.scope) ? pairReq.scope.filter((x) => typeof x === 'string') : null
|
|
152
|
+
if (asked && asked.some((x) => !ALLOWED(x))) {
|
|
153
|
+
writeJson(pairFile, { v: 2, at: Date.now(), error: 'scope not allowed: ' + asked.filter((x) => !ALLOWED(x)).join(',') })
|
|
154
|
+
return console.error('[vault] pairing refused: scope not allowed (%s)', asked.join(','))
|
|
155
|
+
}
|
|
156
|
+
const scope = asked?.length
|
|
157
|
+
? [...new Set(asked)]
|
|
158
|
+
: isService ? ['vault:secrets:' + pairReq.service] : ['vault:sign', 'vault:read', 'vault:store']
|
|
147
159
|
const label = pairReq?.label || (isService ? 'service:' + pairReq.service : 'cli')
|
|
148
160
|
// `profile`/`profileName`: la CUENTA del vault a la que entra el dispositivo.
|
|
149
161
|
// Con varias bóvedas en el mismo daemon, el QR sale de UNA y quien empareja
|
|
@@ -181,6 +193,21 @@ export async function runDaemon () {
|
|
|
181
193
|
const revokeReqFile = path.join(dir, 'revoke-request.json')
|
|
182
194
|
const secretReqFile = path.join(dir, 'secret-request.json')
|
|
183
195
|
const secretsListFile = path.join(dir, 'secrets-list.json')
|
|
196
|
+
/**
|
|
197
|
+
* Por qué falló la última orden de variables. La consola pide el cambio y el volcado
|
|
198
|
+
* por señales distintas, así que el motivo no cabe en la respuesta: se guarda aquí y
|
|
199
|
+
* viaja en el volcado siguiente. Sin esto, una contraseña equivocada se leía como
|
|
200
|
+
* «El daemon no aplicó el cambio», que no dice qué hacer.
|
|
201
|
+
*/
|
|
202
|
+
let lastSecretError = null
|
|
203
|
+
/**
|
|
204
|
+
* El VALOR que se acaba de destapar, esperando al volcado siguiente (mismo camino que
|
|
205
|
+
* `lastSecretError`: la orden y el volcado son señales distintas). Vive en memoria un
|
|
206
|
+
* instante y se va con el volcado — quien lo lee borra el archivo enseguida.
|
|
207
|
+
*/
|
|
208
|
+
let lastSecretValue = null
|
|
209
|
+
/** Y las versiones anteriores que se acaban de pedir. Mismo camino, mismo volcado. */
|
|
210
|
+
let lastSecretHistory = null
|
|
184
211
|
const profileReqFile = path.join(dir, 'profile-request.json')
|
|
185
212
|
const dumpReqFile = path.join(dir, 'dump-request.json')
|
|
186
213
|
const meReqFile = path.join(dir, 'me-request.json')
|
|
@@ -200,6 +227,19 @@ export async function runDaemon () {
|
|
|
200
227
|
* 0700 del vault y se BORRA al consumirla — mismo camino que ya usan los
|
|
201
228
|
* secretos, y así nunca pasa por `ps` ni por el historial de la shell.
|
|
202
229
|
*/
|
|
230
|
+
/**
|
|
231
|
+
* Vuelve a cerrar la copia maestra de los secretos con otra llave. Se llama al poner
|
|
232
|
+
* o quitar la contraseña del perfil: los sobres de las variables NO se tocan (siguen
|
|
233
|
+
* sellados a la llave de cada aparato), solo cambia con qué se abre el llavero de
|
|
234
|
+
* administración. Sin esto, cambiar la contraseña dejaría los secretos ilegibles.
|
|
235
|
+
*/
|
|
236
|
+
async function rekey (id, vieja, nueva) {
|
|
237
|
+
const v = mgr.get(id)
|
|
238
|
+
if (!v?.rekeySecrets) return
|
|
239
|
+
const r = await v.rekeySecrets(vieja, nueva)
|
|
240
|
+
if (r?.rekeyed) console.log('[vault] secrets master re-sealed (%d drawer(s))', r.drawers)
|
|
241
|
+
}
|
|
242
|
+
|
|
203
243
|
async function handleProfileRequest (req) {
|
|
204
244
|
const ref = () => mgr.resolve(req.profile || mgr.currentId())
|
|
205
245
|
switch (req.op) {
|
|
@@ -210,10 +250,54 @@ export async function runDaemon () {
|
|
|
210
250
|
case 'rm': { const r = await mgr.remove(req.profile); return { done: `perfil borrado: ${r.name || r.id}` } }
|
|
211
251
|
case 'rename': { const p = mgr.profiles.rename(ref(), req.name); return { done: `perfil renombrado: ${p.name}` } }
|
|
212
252
|
case 'use': { const p = mgr.profiles.setCurrent(ref()); return { done: `perfil activo: ${p.name || p.id}` } }
|
|
213
|
-
|
|
253
|
+
// ABRIR LA BÓVEDA SALDA LO QUE SE DEBE. Un aparato que entró después de escrita
|
|
254
|
+
// una variable no puede abrirla —envolverle su llave exige abrir la CEK, y eso
|
|
255
|
+
// pide la frase—, así que se queda en deuda y se ve en la consola y en la TUI.
|
|
256
|
+
// Este es el único momento en que la frase está delante, así que es aquí donde se
|
|
257
|
+
// paga; el dueño no tiene que acordarse de un comando aparte.
|
|
258
|
+
case 'unlock': {
|
|
259
|
+
const id = ref()
|
|
260
|
+
await mgr.profiles.unlock(id, req.password)
|
|
261
|
+
let note = ''
|
|
262
|
+
try {
|
|
263
|
+
const ak = await mgr.profiles.adminKey(id, req.password)
|
|
264
|
+
// REHACER el llavero, no solo saldar: con la frase delante se puede dejar cada
|
|
265
|
+
// cajón envuelto para exactamente quien dice el acta — creando lo que falta,
|
|
266
|
+
// reemplazando lo que alguien metiera mal y quitando lo que sobre.
|
|
267
|
+
const r = await mgr.get(id)?.resealAll?.(ak)
|
|
268
|
+
if (r?.wrapped) console.log('[vault] keyring rebuilt on unlock: %d wrap(s) in %d drawer(s)%s',
|
|
269
|
+
r.wrapped, r.drawers, r.dropped ? `, ${r.dropped} stale one(s) dropped` : '')
|
|
270
|
+
if (r?.dropped) note = ` · llavero al día (${r.dropped} envoltura(s) de más retirada(s))`
|
|
271
|
+
else if (r?.wrapped) note = ' · llavero al día'
|
|
272
|
+
} catch (e) { console.error('[vault] could not rebuild the keyring on unlock:', e.message) }
|
|
273
|
+
return { done: 'perfil desbloqueado' + note }
|
|
274
|
+
}
|
|
214
275
|
case 'lock': { mgr.profiles.lock(ref()); return { done: 'perfil bloqueado' } }
|
|
215
|
-
|
|
216
|
-
|
|
276
|
+
// PONER contraseña: los secretos pasan de abrirse con la llave de la máquina a
|
|
277
|
+
// abrirse con la frase. Hay que volver a cerrar la copia maestra con la nueva, o
|
|
278
|
+
// quedarían ilegibles. Si el perfil YA tenía contraseña, hace falta la vieja para
|
|
279
|
+
// poder abrirla: por eso el camino normal para cambiarla es quitarla y ponerla.
|
|
280
|
+
case 'password-set': {
|
|
281
|
+
const id = ref()
|
|
282
|
+
const tenia = !!mgr.profiles.get(id)?.protected
|
|
283
|
+
if (tenia && !req.current) throw new Error('this profile already has a password: remove it first (`profile password --rm`) and then set the new one')
|
|
284
|
+
const vieja = tenia ? await mgr.profiles.adminKey(id, req.current) : null
|
|
285
|
+
await mgr.profiles.setPassword(id, req.password)
|
|
286
|
+
const nueva = await mgr.profiles.adminKey(id, req.password)
|
|
287
|
+
await rekey(id, vieja, nueva)
|
|
288
|
+
return { done: 'contraseña guardada' }
|
|
289
|
+
}
|
|
290
|
+
// QUITARLA: al revés. Se abre la copia maestra con la frase y se vuelve a cerrar
|
|
291
|
+
// con la llave de la máquina, que es la protección de siempre — el disco sigue
|
|
292
|
+
// cifrado, pero su material vive en ese mismo disco.
|
|
293
|
+
case 'password-rm': {
|
|
294
|
+
const id = ref()
|
|
295
|
+
if (!req.password) throw new Error('removing the password needs the current one: the secrets must be re-sealed before it goes')
|
|
296
|
+
const vieja = await mgr.profiles.adminKey(id, req.password)
|
|
297
|
+
await rekey(id, vieja, null)
|
|
298
|
+
mgr.profiles.removePassword(id)
|
|
299
|
+
return { done: 'contraseña quitada · los secretos ahora se abren con la llave de esta máquina' }
|
|
300
|
+
}
|
|
217
301
|
default: throw new Error('unknown profile operation: ' + req.op)
|
|
218
302
|
}
|
|
219
303
|
}
|
|
@@ -278,25 +362,75 @@ export async function runDaemon () {
|
|
|
278
362
|
// 0700 del vault y se borra al consumir.
|
|
279
363
|
const sec = readJsonSafe(secretReqFile)
|
|
280
364
|
if (sec?.op) {
|
|
281
|
-
rm(secretReqFile)
|
|
365
|
+
rm(secretReqFile) // puede llevar la contraseña: fuera del disco cuanto antes
|
|
366
|
+
lastSecretError = null
|
|
282
367
|
try {
|
|
283
368
|
const vault = targetOf(sec)
|
|
284
369
|
// Carga en GRUPO (`secret set ns K=v K2=v2`, `secret import`): todas las
|
|
285
370
|
// variables entran de una vez y sale UN solo aviso de cambio, para que el
|
|
286
371
|
// servicio no se reinicie a media carga y arranque con la mitad puesta.
|
|
287
|
-
|
|
372
|
+
// La CONTRASEÑA, si vino, se convierte en la llave que abre la copia de
|
|
373
|
+
// RECUPERACIÓN, y no se guarda en ningún sitio: se usa y se suelta. Desde v5
|
|
374
|
+
// solo la piden las operaciones que LEEN —ver un valor, cambiar su visibilidad,
|
|
375
|
+
// convertir el archivo, rotar re-cifrando—: escribir no (§8.1). Sin ella se cae
|
|
376
|
+
// a la llave de la máquina, que es la protección de antes de esto (y el vault lo
|
|
377
|
+
// avisa al arrancar).
|
|
378
|
+
const ak = sec.password ? await mgr.profiles.adminKey(sec.profile ? mgr.resolve(sec.profile) : mgr.currentId(), sec.password) : undefined
|
|
379
|
+
if (sec.op === 'migrate') {
|
|
380
|
+
// Sin lista a mano: la pone el vault, y es la MISMA que usa cualquier
|
|
381
|
+
// escritura (servicios del cajón + aparatos que administran). Con una lista
|
|
382
|
+
// propia aquí, lo convertido quedaba sellado solo a los servicios y el dueño
|
|
383
|
+
// no podía ver desde su consola nada de lo que ya tenía.
|
|
384
|
+
const r = await vault.migrateSecrets(null, ak)
|
|
385
|
+
if (!r.migrated) console.log('[vault] nothing to migrate: %s', r.reason)
|
|
386
|
+
else {
|
|
387
|
+
console.log('[vault] secrets SEALED (v%d -> v5). Backup left at secrets.json.v%d.bak', r.from, r.from)
|
|
388
|
+
for (const [owner, sin] of Object.entries(r.sinLlave || {})) {
|
|
389
|
+
console.log('[vault] WARNING %s: %d member(s) without an encryption key will NOT read their variables', owner, sin.length)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} else if (sec.op === 'batch') {
|
|
288
393
|
const changed = sec.pub
|
|
289
|
-
? await vault.applyDeviceSecrets(sec.pub, sec.items)
|
|
290
|
-
: vault.applySecrets(sec.ns, sec.items)
|
|
394
|
+
? await vault.applyDeviceSecrets(sec.pub, sec.items, { by: null })
|
|
395
|
+
: await vault.applySecrets(sec.ns, sec.items, { by: null })
|
|
291
396
|
console.log('[vault] %d secret(s) applied in one go: %s', changed.length, sec.pub ? 'device' : sec.ns)
|
|
292
|
-
} else if (sec.op === 'set') { vault.setSecret(sec.ns, sec.key, sec.value, sec.public); console.log('[vault] secret saved: %s/%s', sec.ns, sec.key) }
|
|
293
|
-
else if (sec.op === 'rm') { vault.deleteSecret(sec.ns, sec.key); console.log('[vault] secret deleted: %s/%s', sec.ns, sec.key) }
|
|
397
|
+
} 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) }
|
|
398
|
+
else if (sec.op === 'rm') { await vault.deleteSecret(sec.ns, sec.key); console.log('[vault] secret deleted: %s/%s', sec.ns, sec.key) }
|
|
294
399
|
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) }
|
|
295
400
|
else if (sec.op === 'dev-rm') { await vault.deleteDeviceSecret(sec.pub, sec.key); console.log('[vault] device secret deleted: %s', sec.key) }
|
|
401
|
+
// Saldar lo que quedó a deber: heredarle a un aparato nuevo lo ya guardado y
|
|
402
|
+
// rotar de verdad el cajón del que salió alguien. Las dos cosas abren, así que
|
|
403
|
+
// van con la frase — y por eso se hacen aquí y no al escribir.
|
|
404
|
+
else if (sec.op === 'settle') {
|
|
405
|
+
const r = await vault.settleSecretDebts(ak)
|
|
406
|
+
const n = Object.keys(r).length
|
|
407
|
+
console.log(n ? `[vault] ${n} pending drawer(s) settled` : '[vault] nothing pending')
|
|
408
|
+
}
|
|
409
|
+
// Ver el valor de una privada: lo único que la frase guarda (§8.3).
|
|
410
|
+
else if (sec.op === 'reveal') {
|
|
411
|
+
const value = await vault.revealSecret(sec.owner, sec.key, ak)
|
|
412
|
+
lastSecretValue = { owner: sec.owner, key: sec.key, value }
|
|
413
|
+
}
|
|
414
|
+
// Qué versiones anteriores hay (sin valores: son sobres).
|
|
415
|
+
else if (sec.op === 'history') {
|
|
416
|
+
lastSecretHistory = { owner: sec.owner || null, key: sec.key || null, items: vault.secretHistory(sec.owner || null, sec.key || null) }
|
|
417
|
+
}
|
|
418
|
+
// REVERTIR: abrir la versión vieja (frase) y volver a guardarla (nada).
|
|
419
|
+
else if (sec.op === 'revert') {
|
|
420
|
+
const ok = await vault.revertSecret(sec.owner, sec.key, sec.ts, { adminKey: ak })
|
|
421
|
+
if (!ok) throw new Error('that version is not in the history any more')
|
|
422
|
+
console.log('[vault] secret reverted: %s/%s', sec.owner, sec.key)
|
|
423
|
+
}
|
|
296
424
|
// Visibilidad: si el valor puede salir hacia la consola remota. No toca el valor.
|
|
297
|
-
else if (sec.op === 'vis') { vault.setSecretVisibility(sec.ns, sec.key, sec.public); console.log('[vault] secret visibility: %s/%s → %s', sec.ns, sec.key, sec.public ? 'public' : 'private') }
|
|
298
|
-
else if (sec.op === 'dev-vis') { await vault.setDeviceSecretVisibility(sec.pub, sec.key, sec.public); console.log('[vault] device secret visibility: %s → %s', sec.key, sec.public ? 'public' : 'private') }
|
|
299
|
-
} catch (e) {
|
|
425
|
+
else if (sec.op === 'vis') { await vault.setSecretVisibility(sec.ns, sec.key, sec.public, ak); console.log('[vault] secret visibility: %s/%s → %s', sec.ns, sec.key, sec.public ? 'public' : 'private') }
|
|
426
|
+
else if (sec.op === 'dev-vis') { await vault.setDeviceSecretVisibility(sec.pub, sec.key, sec.public, ak); console.log('[vault] device secret visibility: %s → %s', sec.key, sec.public ? 'public' : 'private') }
|
|
427
|
+
} catch (e) {
|
|
428
|
+
lastSecretError = {
|
|
429
|
+
error: e.message,
|
|
430
|
+
code: e.code || (/wrong password/i.test(e.message) ? 'WRONG_PASSWORD' : 'SECRET_FAILED')
|
|
431
|
+
}
|
|
432
|
+
console.error('[vault] secret failed:', e.message)
|
|
433
|
+
}
|
|
300
434
|
}
|
|
301
435
|
// Perfiles / candado.
|
|
302
436
|
const preq = readJsonSafe(profileReqFile)
|
|
@@ -376,8 +510,21 @@ export async function runDaemon () {
|
|
|
376
510
|
writeJson(secretsListFile, {
|
|
377
511
|
v: 2, at: Date.now(), req: reqId, profile: t.id,
|
|
378
512
|
ns: t.vault.listSecrets(),
|
|
379
|
-
dev: await t.vault.listDeviceSecrets()
|
|
513
|
+
dev: await t.vault.listDeviceSecrets(),
|
|
514
|
+
// Lo que quedó a deber un sellado. Va en el volcado porque si no se ve, no se
|
|
515
|
+
// salda: son cajones cuyos miembros NO están leyendo sus variables.
|
|
516
|
+
pending: await t.vault.secretDebts(),
|
|
517
|
+
// Y quién NO puede abrir lo suyo. Es lo mismo visto desde el aparato, que es
|
|
518
|
+
// como lo mira quien administra: «este servicio está en el acta y aun así no
|
|
519
|
+
// arranca». Sin esto solo se veía en el log del propio servicio.
|
|
520
|
+
incomplete: await t.vault.incompleteMembers(),
|
|
521
|
+
...(lastSecretError ? { secretError: lastSecretError } : {}),
|
|
522
|
+
...(lastSecretValue ? { revealed: lastSecretValue } : {}),
|
|
523
|
+
...(lastSecretHistory ? { history: lastSecretHistory } : {})
|
|
380
524
|
})
|
|
525
|
+
lastSecretError = null
|
|
526
|
+
lastSecretValue = null
|
|
527
|
+
lastSecretHistory = null
|
|
381
528
|
writeJson(devFile, { v: 1, at: Date.now(), req: reqId, profile: t.id, ...(await t.vault.listDevices()) })
|
|
382
529
|
// Acta del perfil: quién es del perfil y qué puede hacer cada uno (`members`/`caps`).
|
|
383
530
|
try { writeJson(path.join(dir, 'acta.json'), { v: 1, at: Date.now(), req: reqId, profile: t.id, ...(await t.vault.profileMembers()) }) } catch (_) {}
|