@dotrino/vaultd 0.7.4 → 0.7.5
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/enroll.js +20 -4
- package/package.json +1 -1
- package/src/daemon.js +7 -3
- package/src/manager.js +37 -0
- package/src/tui/app.js +2 -1
- package/src/tui/i18n.js +4 -2
- package/src/vault.js +2 -0
- package/src/vaultControl.js +1 -1
package/lib/src/enroll.js
CHANGED
|
@@ -108,13 +108,29 @@ export function createEnrollDesk ({
|
|
|
108
108
|
const reply = (to, obj) => { try { send(to, obj) } catch (e) { log('[vault] no se pudo responder:', e.message) } }
|
|
109
109
|
const isFresh = (d) => typeof d?.ts === 'number' && Math.abs(Date.now() - d.ts) <= FRESH_WINDOW_MS
|
|
110
110
|
|
|
111
|
-
/**
|
|
112
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Inicia un emparejamiento: token + nonce de sesión. NO firma nada todavía.
|
|
113
|
+
*
|
|
114
|
+
* `mode` y `account` son LO QUE LA BÓVEDA DECLARA que va a pasar, y viajan en el QR
|
|
115
|
+
* para que el aparato pueda **decirlo antes de hacerlo** en vez de emparejar a
|
|
116
|
+
* ciegas (decisión V9 de `docs/vinculacion-de-cuentas.md`: pregunta el vault, el
|
|
117
|
+
* dispositivo muestra el proceso y sus consecuencias):
|
|
118
|
+
*
|
|
119
|
+
* · `mode: 'join'` → el dispositivo estrena una cuenta suya y entra a la de la
|
|
120
|
+
* bóveda. Es lo único que existe hoy.
|
|
121
|
+
* · `mode: 'adopt'` → la bóveda se quedaría con la cuenta que trae el aparato
|
|
122
|
+
* (camino A). Reservado: todavía no hay protocolo.
|
|
123
|
+
* · `account` → cómo se llama la cuenta de la bóveda, para nombrarla en el
|
|
124
|
+
* aviso. Es ORIENTATIVO (un nombre que puso su dueño); la
|
|
125
|
+
* identidad de verdad de la cuenta es `iss`.
|
|
126
|
+
*/
|
|
127
|
+
function startPairing ({ scope = defaultScope, ttlMs = defaultTtlMs, label = '', mode = 'join', account = '' } = {}) {
|
|
113
128
|
pending.clear() // uno a la vez: una sesión nueva supersede a la anterior
|
|
114
129
|
const token = randToken()
|
|
115
130
|
const sn = randToken()
|
|
116
|
-
|
|
117
|
-
|
|
131
|
+
const acct = String(account || '').slice(0, 40)
|
|
132
|
+
pending.set(token, { token, exp: Date.now() + PAIRING_TTL_MS, scope, ttlMs, label, sn, mode, account: acct, state: 'AWAITING_ENROLL' })
|
|
133
|
+
return { token, qr: { v: 2, iss, proxy, token, sn, m: mode, ...(acct ? { acct } : {}) }, expiresInMs: PAIRING_TTL_MS }
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
function stopPairing (token) { pending.delete(token) }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
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/daemon.js
CHANGED
|
@@ -117,11 +117,13 @@ export async function runDaemon () {
|
|
|
117
117
|
const isService = typeof pairReq?.service === 'string' && pairReq.service
|
|
118
118
|
const scope = isService ? ['vault:secrets:' + pairReq.service] : ['vault:sign', 'vault:read', 'vault:store']
|
|
119
119
|
const label = pairReq?.label || (isService ? 'servicio:' + pairReq.service : 'cli')
|
|
120
|
-
const { qr, expiresInMs } = vault.startPairing({ scope, label, ttlMs: DEVICE_TTL_MS })
|
|
121
120
|
// `profile`/`profileName`: la CUENTA del vault a la que entra el dispositivo.
|
|
122
121
|
// Con varias bóvedas en el mismo daemon, el QR sale de UNA y quien empareja
|
|
123
|
-
// tiene que verlo (lo muestran la TUI y `dotrino-vault pair`).
|
|
122
|
+
// tiene que verlo (lo muestran la TUI y `dotrino-vault pair`). El nombre viaja
|
|
123
|
+
// TAMBIÉN dentro del QR (`acct`) para que el dispositivo pueda anunciar qué va
|
|
124
|
+
// a pasar antes de hacerlo (V9 de docs/vinculacion-de-cuentas.md).
|
|
124
125
|
const profileName = mgr.profiles.get(profileId)?.name || ''
|
|
126
|
+
const { qr, expiresInMs } = vault.startPairing({ scope, label, ttlMs: DEVICE_TTL_MS, mode: 'join', account: profileName })
|
|
125
127
|
writeJson(pairFile, { v: 2, qr, expiresAt: Date.now() + expiresInMs, profile: profileId, profileName })
|
|
126
128
|
// El token es un secreto efímero: no debe quedar en disco más allá de su
|
|
127
129
|
// vida. Se borra al VENCER (aquí) y al APROBARSE (abajo, consumido).
|
|
@@ -217,7 +219,9 @@ export async function runDaemon () {
|
|
|
217
219
|
rm(profileReqFile) // lleva la contraseña: fuera del disco cuanto antes
|
|
218
220
|
let extra = {}
|
|
219
221
|
try { extra = await handleProfileRequest(preq) }
|
|
220
|
-
|
|
222
|
+
// `code`: la TUI es bilingüe y traduce por código (un freno como el D12 tiene
|
|
223
|
+
// que leerse en el idioma de quien lo lee, no en el del daemon).
|
|
224
|
+
catch (e) { extra = { error: e.message, ...(e.code ? { code: e.code } : {}) }; console.error('[vault] perfil: %s', e.message) }
|
|
221
225
|
dumpProfiles(extra)
|
|
222
226
|
} else {
|
|
223
227
|
dumpProfiles()
|
package/src/manager.js
CHANGED
|
@@ -13,6 +13,28 @@ import { openProfiles } from './profiles.js'
|
|
|
13
13
|
import { installNodeGlobals } from './node-globals.js'
|
|
14
14
|
import { dataDir, ensureDir } from './paths.js'
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* D12 (`docs/acta-de-perfil.md`): la bóveda **no** borra una cuenta que ella manda si
|
|
18
|
+
* quedan otros miembros — antes tiene que pasarle el acta a un dispositivo conectado.
|
|
19
|
+
* Es D6 ("perder el master es perder la cuenta") leído al derecho: con más miembros,
|
|
20
|
+
* el que borra no la pierde solo para él.
|
|
21
|
+
*
|
|
22
|
+
* Pura a propósito (recibe el veredicto ya calculado) para poder probar la regla sin
|
|
23
|
+
* levantar un vault entero.
|
|
24
|
+
*/
|
|
25
|
+
export function assertCanRemove ({ isMaster, memberCount, name = '' }) {
|
|
26
|
+
if (!isMaster || memberCount <= 1) return true
|
|
27
|
+
const otros = memberCount - 1
|
|
28
|
+
const e = new Error(
|
|
29
|
+
`la cuenta "${name}" la manda esta bóveda y tiene ${otros} dispositivo(s) más: ` +
|
|
30
|
+
'pásale primero el mando a uno que esté conectado. Si la borras así, se quedan ' +
|
|
31
|
+
'con su llave y sin nadie que pueda volver a firmar el acta.'
|
|
32
|
+
)
|
|
33
|
+
e.code = 'MASTER_WITH_MEMBERS'
|
|
34
|
+
e.members = memberCount
|
|
35
|
+
throw e
|
|
36
|
+
}
|
|
37
|
+
|
|
16
38
|
export async function startVaultManager ({ root = dataDir(), proxyUrl, log = console.log, onEnrollChallenge } = {}) {
|
|
17
39
|
ensureDir(root)
|
|
18
40
|
// El keypair de transporte del proxy-client es del PROCESO, no de la identidad:
|
|
@@ -76,6 +98,21 @@ export async function startVaultManager ({ root = dataDir(), proxyUrl, log = con
|
|
|
76
98
|
/** Borra el perfil: cierra su conexión y elimina su maestra y sus datos. */
|
|
77
99
|
async remove (ref) {
|
|
78
100
|
const id = profiles.resolve(ref)
|
|
101
|
+
// FRENO D12 (acta-de-perfil.md): si esta bóveda MANDA la cuenta y quedan otros
|
|
102
|
+
// miembros, borrarla los deja con su llave y sin nadie que pueda volver a sellar
|
|
103
|
+
// el acta: la cuenta muere para todos, en silencio. Primero se le pasa el mando
|
|
104
|
+
// a un dispositivo conectado. (En el dispositivo no hay tal freno: allí borrar
|
|
105
|
+
// se lleva su llave y su copia, y la cuenta sigue viva donde vive el master.)
|
|
106
|
+
const v = running.get(id)
|
|
107
|
+
if (v) {
|
|
108
|
+
const [soyMaster, acta] = await Promise.all([
|
|
109
|
+
v.isMaster().catch(() => false),
|
|
110
|
+
v.profileMembers().catch(() => ({ members: [] }))
|
|
111
|
+
])
|
|
112
|
+
assertCanRemove({ isMaster: soyMaster, memberCount: (acta?.members || []).length, name: profiles.get(id)?.name || id })
|
|
113
|
+
} else {
|
|
114
|
+
log('[vault] perfil %s no está abierto: se borra sin poder comprobar su acta', id)
|
|
115
|
+
}
|
|
79
116
|
const res = profiles.remove(id) // valida: no es el único, no está bloqueado
|
|
80
117
|
try { running.get(id)?.close() } catch (_) {}
|
|
81
118
|
running.delete(id)
|
package/src/tui/app.js
CHANGED
|
@@ -50,7 +50,8 @@ function humanErr (e, st) {
|
|
|
50
50
|
NO_REPLY: t.errNoReply,
|
|
51
51
|
NOT_APPLIED: t.errNotApplied,
|
|
52
52
|
NOT_DELETED: t.errNotDeleted,
|
|
53
|
-
PAIR_FAILED: t.errPairFailed
|
|
53
|
+
PAIR_FAILED: t.errPairFailed,
|
|
54
|
+
MASTER_WITH_MEMBERS: t.errMasterWithMembers
|
|
54
55
|
}
|
|
55
56
|
return byCode[e?.code] || e?.message || String(e)
|
|
56
57
|
}
|
package/src/tui/i18n.js
CHANGED
|
@@ -179,7 +179,8 @@ const es = {
|
|
|
179
179
|
errNoReply: 'El daemon no respondió.',
|
|
180
180
|
errNotApplied: 'El daemon no aplicó el cambio (revisa los logs del servicio).',
|
|
181
181
|
errNotDeleted: 'El daemon no borró la variable (revisa los logs del servicio).',
|
|
182
|
-
errPairFailed: 'El daemon no inició el emparejamiento.'
|
|
182
|
+
errPairFailed: 'El daemon no inició el emparejamiento.',
|
|
183
|
+
errMasterWithMembers: 'Esta cuenta la manda esta bóveda y tiene otros dispositivos: pásale primero el mando a uno que esté conectado. Si la borras así, se quedan con su llave y sin nadie que pueda volver a firmar el acta.'
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
// ---------------------------------- inglés ----------------------------------
|
|
@@ -333,7 +334,8 @@ const en = {
|
|
|
333
334
|
errNoReply: 'The daemon did not answer.',
|
|
334
335
|
errNotApplied: 'The daemon did not apply the change (check the service logs).',
|
|
335
336
|
errNotDeleted: 'The daemon did not delete the variable (check the service logs).',
|
|
336
|
-
errPairFailed: 'The daemon did not start the pairing.'
|
|
337
|
+
errPairFailed: 'The daemon did not start the pairing.',
|
|
338
|
+
errMasterWithMembers: 'This vault is in charge of this account and it has other devices: hand the lead over to one that is online first. If you delete it like this, they keep their key with nobody able to sign the record again.'
|
|
337
339
|
}
|
|
338
340
|
|
|
339
341
|
// --------------------------- selección y persistencia -----------------------
|
package/src/vault.js
CHANGED
|
@@ -296,6 +296,8 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
296
296
|
// Acta del perfil (quién es del perfil y qué puede cada uno): lo que muestran
|
|
297
297
|
// `dotrino-vault members` y la consola de vault.dotrino.com.
|
|
298
298
|
profileMembers: () => identity.profileMembers(),
|
|
299
|
+
// ¿Es ESTA bóveda la que sella el acta? Lo usa el freno de borrado (D12).
|
|
300
|
+
isMaster: () => identity.isMaster(),
|
|
299
301
|
setCaps: (pub, caps) => identity.setCaps(pub, caps),
|
|
300
302
|
revokeDevice: (nonce) => desk.revoke(nonce),
|
|
301
303
|
close () { try { client.close() } catch (_) {} identity.destroy() }
|
package/src/vaultControl.js
CHANGED
|
@@ -149,7 +149,7 @@ async function profileOp (op, { profile, name, password } = {}) {
|
|
|
149
149
|
signalOrCleanup('SIGUSR2', [F.profileReq])
|
|
150
150
|
const d = await waitFor(F.profilesList)
|
|
151
151
|
if (!d) throw coded('el daemon no respondió', 'NO_REPLY')
|
|
152
|
-
if (d.error) throw
|
|
152
|
+
if (d.error) throw coded(d.error, d.code) // p.ej. MASTER_WITH_MEMBERS (freno D12)
|
|
153
153
|
return d // { profiles:[{id,name,protected,locked,current,fingerprint,iss,createdAt}], current, done? }
|
|
154
154
|
}
|
|
155
155
|
|