@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vaultd",
3
- "version": "0.13.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 } })