@dotrino/vaultd 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/ctl.js +64 -0
- package/src/daemon.js +34 -0
- package/src/vault.js +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Certificador personal de Dotrino: daemon headless que custodia la clave maestra y delega capacidades a tus dispositivos por el proxy. Tu CA propia.",
|
|
6
6
|
"bin": {
|
package/src/ctl.js
CHANGED
|
@@ -249,6 +249,68 @@ function cmdReject (deviceId) {
|
|
|
249
249
|
console.log('Rechazado %s.', deviceId)
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/**
|
|
253
|
+
* `dotrino-vault me` — el PERFIL del usuario tal como lo tiene la bóveda: apodo, foto y
|
|
254
|
+
* datos. Es lo que editas en cualquier dispositivo emparejado y se sincroniza aquí, así
|
|
255
|
+
* que sirve para comprobar que lo que cambiaste en el aparato llegó de verdad.
|
|
256
|
+
*
|
|
257
|
+
* Distinto de `members` (quién es del perfil) y de `profile` (los perfiles del PC): esto
|
|
258
|
+
* es el CONTENIDO, no la identidad.
|
|
259
|
+
*
|
|
260
|
+
* La foto no se imprime —es un data-URI de hasta ~90 KB— sino que se resume;
|
|
261
|
+
* `me --foto <archivo>` la escribe en disco para poder mirarla.
|
|
262
|
+
*/
|
|
263
|
+
async function cmdMe (args = []) {
|
|
264
|
+
const i = args.findIndex((a) => a === '--foto' || a === '--photo')
|
|
265
|
+
const avatarPath = i >= 0 ? args[i + 1] : null
|
|
266
|
+
if (i >= 0 && !avatarPath) { console.error('uso: dotrino-vault me --foto <archivo>'); process.exit(2) }
|
|
267
|
+
|
|
268
|
+
const s = requireDaemon()
|
|
269
|
+
const meFile = path.join(dir, 'me.json')
|
|
270
|
+
try { fs.rmSync(meFile, { force: true }) } catch (_) {}
|
|
271
|
+
writeReq('me-request.json', { ...(avatarPath ? { avatarPath: path.resolve(avatarPath) } : {}) })
|
|
272
|
+
avisar(s.pid, 'SIGUSR2')
|
|
273
|
+
let dump = null
|
|
274
|
+
for (let n = 0; n < 50; n++) { await sleep(100); const d = readJson(meFile, null); if (d?.at) { dump = d; break } }
|
|
275
|
+
// El volcado es contenido del usuario: se lee y se BORRA, no se queda ahí suelto.
|
|
276
|
+
try { fs.rmSync(meFile, { force: true }) } catch (_) {}
|
|
277
|
+
if (!dump) { console.error('La bóveda no respondió. ¿Está corriendo? dotrino-vault status'); process.exit(1) }
|
|
278
|
+
|
|
279
|
+
const me = dump.me
|
|
280
|
+
if (!me) {
|
|
281
|
+
console.log('\nTodavía no hay perfil en esta bóveda.')
|
|
282
|
+
console.log('Edita tu nombre o tu foto en un dispositivo emparejado y vuelve a mirar.\n')
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const cuando = me.updatedAt ? new Date(me.updatedAt).toLocaleString() : '—'
|
|
287
|
+
console.log('\n%sPerfil%s · actualizado %s\n', B, Z, cuando)
|
|
288
|
+
console.log(' nombre : %s', me.nickname || '(sin nombre)')
|
|
289
|
+
console.log(' foto : %s', me.avatar
|
|
290
|
+
? `sí · ${me.avatar.type || 'desconocido'} · ${(me.avatar.bytes / 1024).toFixed(1)} KB`
|
|
291
|
+
: 'no')
|
|
292
|
+
|
|
293
|
+
// Los campos estándar. `visible` es del usuario: teléfono y dirección nacen ocultos.
|
|
294
|
+
const STD = [['nombres', 'nombres'], ['apellidos', 'apellidos'], ['email', 'correo'],
|
|
295
|
+
['telefono', 'teléfono'], ['direccion', 'dirección']]
|
|
296
|
+
const puestos = STD.filter(([k]) => me[k])
|
|
297
|
+
if (puestos.length) {
|
|
298
|
+
console.log('')
|
|
299
|
+
for (const [k, etiqueta] of puestos) {
|
|
300
|
+
console.log(' %s: %s%s', etiqueta.padEnd(12), me[k], me[k + 'Visible'] === false ? ' (oculto)' : '')
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
for (const [titulo, lista] of [['Enlaces', me.links], ['Otros datos', me.fields]]) {
|
|
304
|
+
if (!Array.isArray(lista) || !lista.length) continue
|
|
305
|
+
console.log('\n %s:', titulo)
|
|
306
|
+
for (const x of lista) console.log(' %s %s%s', (x.type || x.label || '').padEnd(12), x.value, x.visible === false ? ' (oculto)' : '')
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (dump.avatarGuardada) console.log('\n Foto escrita en: %s', dump.avatarGuardada)
|
|
310
|
+
else if (me.avatar) console.log('\n Para verla: dotrino-vault me --foto ~/perfil.png')
|
|
311
|
+
console.log('')
|
|
312
|
+
}
|
|
313
|
+
|
|
252
314
|
/**
|
|
253
315
|
* `dotrino-vault members` — el ACTA del perfil: qué llaves son tuyas y qué puede hacer cada
|
|
254
316
|
* una. Es la misma información que muestra la consola de vault.dotrino.com.
|
|
@@ -584,6 +646,7 @@ function help () {
|
|
|
584
646
|
approve <código> aprueba el dispositivo tipeando el código que MUESTRA (el vault no lo sabe)
|
|
585
647
|
reject <deviceId> rechaza un dispositivo pendiente
|
|
586
648
|
devices lista dispositivos enrolados / revocados
|
|
649
|
+
me tu perfil (nombre, foto, datos) tal como lo tiene la bóveda
|
|
587
650
|
members el acta del perfil: quién es tuyo y qué puede hacer
|
|
588
651
|
caps <ID> ±permiso cambia permisos (+firma -guarda +administra …)
|
|
589
652
|
revoke <nonce> revoca un dispositivo (le ordena autoborrarse)
|
|
@@ -628,6 +691,7 @@ export async function runCtl (argv) {
|
|
|
628
691
|
case 'approve': return cmdApprove(rest[0])
|
|
629
692
|
case 'reject': return cmdReject(rest[0])
|
|
630
693
|
case 'devices': return cmdDevices()
|
|
694
|
+
case 'me': return cmdMe(rest)
|
|
631
695
|
case 'members': return cmdMembers()
|
|
632
696
|
case 'caps': return cmdCaps(rest)
|
|
633
697
|
case 'revoke': return cmdRevoke(rest[0])
|
package/src/daemon.js
CHANGED
|
@@ -154,6 +154,16 @@ export async function runDaemon () {
|
|
|
154
154
|
const secretsListFile = path.join(dir, 'secrets-list.json')
|
|
155
155
|
const profileReqFile = path.join(dir, 'profile-request.json')
|
|
156
156
|
const dumpReqFile = path.join(dir, 'dump-request.json')
|
|
157
|
+
const meReqFile = path.join(dir, 'me-request.json')
|
|
158
|
+
const meFile = path.join(dir, 'me.json')
|
|
159
|
+
|
|
160
|
+
/** Resumen de la foto de perfil: qué es y cuánto pesa, nunca los bytes. */
|
|
161
|
+
function avatarInfo (avatar) {
|
|
162
|
+
if (typeof avatar !== 'string' || !avatar) return null
|
|
163
|
+
const m = /^data:([^;,]+)?(?:;base64)?,(.*)$/s.exec(avatar)
|
|
164
|
+
if (!m) return { type: null, bytes: avatar.length }
|
|
165
|
+
return { type: m[1] || 'desconocido', bytes: Math.floor(m[2].length * 3 / 4) }
|
|
166
|
+
}
|
|
157
167
|
|
|
158
168
|
/**
|
|
159
169
|
* Órdenes de perfil (crear/renombrar/borrar/activar) y del candado
|
|
@@ -242,6 +252,30 @@ export async function runDaemon () {
|
|
|
242
252
|
writeJson(devFile, { v: 1, at: Date.now(), profile: t.id, ...(await t.vault.listDevices()) })
|
|
243
253
|
// Acta del perfil: quién es del perfil y qué puede hacer cada uno (`members`/`caps`).
|
|
244
254
|
try { writeJson(path.join(dir, 'acta.json'), { v: 1, at: Date.now(), profile: t.id, ...(await t.vault.profileMembers()) }) } catch (_) {}
|
|
255
|
+
|
|
256
|
+
// PERFIL del usuario (apodo, foto, datos) tal como lo tiene la bóveda: `dotrino-vault me`.
|
|
257
|
+
// Solo se vuelca cuando se PIDE, no en cada señal: es contenido del usuario y no tiene
|
|
258
|
+
// por qué quedar escrito en un archivo suelto cada vez que alguien mira los miembros.
|
|
259
|
+
// La FOTO no entra en el volcado (son hasta ~90 KB de data-URI que nadie va a leer en
|
|
260
|
+
// una terminal): se resume, y si la quieres, `--foto <archivo>` la escribe donde digas.
|
|
261
|
+
const meReq = readJsonSafe(meReqFile)
|
|
262
|
+
if (meReq) {
|
|
263
|
+
rm(meReqFile)
|
|
264
|
+
try {
|
|
265
|
+
const tm = resolveTarget(meReq) || { id: mgr.currentId(), vault: mgr.current() }
|
|
266
|
+
const { me } = tm.vault.threads.methods.profileGet()
|
|
267
|
+
const { avatar, ...resto } = me || {}
|
|
268
|
+
let guardada = null
|
|
269
|
+
if (meReq.avatarPath && typeof avatar === 'string') {
|
|
270
|
+
const m = /^data:([^;,]+)?(?:;base64)?,(.*)$/s.exec(avatar)
|
|
271
|
+
if (m) {
|
|
272
|
+
fs.writeFileSync(meReq.avatarPath, Buffer.from(m[2], 'base64'), { mode: 0o600 })
|
|
273
|
+
guardada = meReq.avatarPath
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
writeJson(meFile, { v: 1, at: Date.now(), profile: tm.id, me: me ? { ...resto, avatar: avatarInfo(avatar) } : null, avatarGuardada: guardada })
|
|
277
|
+
} catch (e) { console.error('[vault] could not dump the profile:', e.message) }
|
|
278
|
+
}
|
|
245
279
|
} catch (e) {
|
|
246
280
|
console.error('[vault] error handling a control signal:', e.message)
|
|
247
281
|
}
|
package/src/vault.js
CHANGED
|
@@ -200,6 +200,13 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
200
200
|
args = JSON.parse(await identity.openContent(d.enc))
|
|
201
201
|
}
|
|
202
202
|
const result = await threads.methods[d.method](args)
|
|
203
|
+
// Que un aparato ESCRIBA en tu bóveda queda anotado. Antes solo se auditaba el
|
|
204
|
+
// rechazo, así que la bitácora contaba quién entró pero no qué hizo después.
|
|
205
|
+
// Solo la operación y el aparato: nunca el contenido (`activity` es un registro
|
|
206
|
+
// de seguridad, no una copia de lo que guardas).
|
|
207
|
+
if (!STORE_READ_METHODS.has(d.method)) {
|
|
208
|
+
audit('store', { device: await deviceIdOf(chk.device), method: d.method })
|
|
209
|
+
}
|
|
203
210
|
if (cek) {
|
|
204
211
|
const enc = await identity.sealContent(JSON.stringify(result ?? null))
|
|
205
212
|
return reply(from, { type: MSG.STORE_RESULT, method: d.method, result: { __enc: enc } })
|