@dotrino/vaultd 0.13.0 → 0.15.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/tui/app.js +80 -4
- package/src/tui/i18n.js +48 -0
- package/src/vault.js +7 -0
- package/src/vaultControl.js +40 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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/tui/app.js
CHANGED
|
@@ -231,6 +231,47 @@ function secretRows (st, t) {
|
|
|
231
231
|
return rows
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* PERFIL del usuario: nombre, foto y datos, tal como los tiene la bóveda. Es lo que se
|
|
236
|
+
* edita en un dispositivo emparejado y se sincroniza aquí; esta pantalla sirve para
|
|
237
|
+
* comprobar que llegó.
|
|
238
|
+
*
|
|
239
|
+
* Solo lectura A PROPÓSITO: el perfil se edita donde lo usas (el aparato), no en el
|
|
240
|
+
* servidor donde vive la bóveda. Aquí se mira.
|
|
241
|
+
*/
|
|
242
|
+
function meRows (st, t) {
|
|
243
|
+
const i = L(st)
|
|
244
|
+
const me = st.me
|
|
245
|
+
if (me === undefined) return [{ text: t.muted(i.loading), sel: false }]
|
|
246
|
+
if (!me) return [{ text: t.muted(i.noProfile), sel: false }, { text: '', sel: false }, { text: t.muted(i.noProfileHint), sel: false }]
|
|
247
|
+
|
|
248
|
+
const rows = []
|
|
249
|
+
const campo = (etiqueta, valor, oculto) => rows.push({
|
|
250
|
+
text: ` ${t.muted(String(etiqueta).padEnd(12))} ${valor}${oculto ? t.muted(i.hidden) : ''}`, sel: false
|
|
251
|
+
})
|
|
252
|
+
rows.push({ text: t.muted(i.profileUpdated(me.updatedAt ? new Date(me.updatedAt).toLocaleString() : '—')), sel: false })
|
|
253
|
+
rows.push({ text: '', sel: false })
|
|
254
|
+
campo(i.fieldName, me.nickname ? t.bold(me.nickname) : t.muted(i.noName))
|
|
255
|
+
campo(i.fieldPhoto, me.avatar
|
|
256
|
+
? `${me.avatar.type || '?'} · ${(me.avatar.bytes / 1024).toFixed(1)} KB`
|
|
257
|
+
: t.muted(i.no))
|
|
258
|
+
|
|
259
|
+
const STD = [['nombres', i.fieldFirstName], ['apellidos', i.fieldLastName], ['email', i.fieldEmail],
|
|
260
|
+
['telefono', i.fieldPhone], ['direccion', i.fieldAddress]]
|
|
261
|
+
const puestos = STD.filter(([k]) => me[k])
|
|
262
|
+
if (puestos.length) rows.push({ text: '', sel: false })
|
|
263
|
+
for (const [k, etiqueta] of puestos) campo(etiqueta, me[k], me[k + 'Visible'] === false)
|
|
264
|
+
|
|
265
|
+
for (const [titulo, lista] of [[i.links, me.links], [i.otherData, me.fields]]) {
|
|
266
|
+
if (!Array.isArray(lista) || !lista.length) continue
|
|
267
|
+
rows.push({ text: '', sel: false })
|
|
268
|
+
rows.push({ text: t.accent(' ▸ ' + titulo), sel: false })
|
|
269
|
+
for (const x of lista) campo(x.type || x.label || '', x.value, x.visible === false)
|
|
270
|
+
}
|
|
271
|
+
if (me.avatar) { rows.push({ text: '', sel: false }); rows.push({ text: t.muted(i.savePhotoHint), sel: false }) }
|
|
272
|
+
return rows
|
|
273
|
+
}
|
|
274
|
+
|
|
234
275
|
// --------------------------------- entrada ---------------------------------
|
|
235
276
|
|
|
236
277
|
function setInput (st, opts) {
|
|
@@ -266,6 +307,10 @@ async function refreshSecrets (term, st) {
|
|
|
266
307
|
const r = await guard(term, st, L(st).loadingSecrets, () => vc.listSecrets(activeId(st)))
|
|
267
308
|
if (r.ok) st.secrets = r.v
|
|
268
309
|
}
|
|
310
|
+
async function refreshMe (term, st) {
|
|
311
|
+
const r = await guard(term, st, L(st).loadingProfile, () => vc.getMe(activeId(st)))
|
|
312
|
+
st.me = r.ok ? r.v : null
|
|
313
|
+
}
|
|
269
314
|
async function refreshProfiles (term, st) {
|
|
270
315
|
const r = await guard(term, st, L(st).loadingVaults, () => vc.listProfiles())
|
|
271
316
|
if (r.ok) st.profiles = r.v
|
|
@@ -557,6 +602,31 @@ async function onKeyPairing (term, st, key) {
|
|
|
557
602
|
return true
|
|
558
603
|
}
|
|
559
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Teclas del PERFIL: refrescar y guardar la foto. Nada de editar — el perfil se edita en
|
|
607
|
+
* el dispositivo que usas, no en la máquina donde vive la bóveda.
|
|
608
|
+
*/
|
|
609
|
+
async function onKeyMe (term, st, key) {
|
|
610
|
+
const i = L(st)
|
|
611
|
+
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
612
|
+
if (ch === 'r') {
|
|
613
|
+
await refreshMe(term, st)
|
|
614
|
+
} else if (ch === 'f' && st.me?.avatar) {
|
|
615
|
+
setInput(st, {
|
|
616
|
+
label: i.savePhotoLabel,
|
|
617
|
+
hint: i.savePhotoHintInput,
|
|
618
|
+
value: 'perfil.png',
|
|
619
|
+
onSubmit: async (valor) => {
|
|
620
|
+
const destino = String(valor || '').trim()
|
|
621
|
+
if (!destino) return
|
|
622
|
+
const r = await guard(term, st, i.savingPhoto, () => vc.saveAvatar(destino, activeId(st)))
|
|
623
|
+
if (r.ok && r.v) flash(st, i.photoSaved(r.v))
|
|
624
|
+
}
|
|
625
|
+
})
|
|
626
|
+
}
|
|
627
|
+
return true
|
|
628
|
+
}
|
|
629
|
+
|
|
560
630
|
async function onKeySecrets (term, st, key) {
|
|
561
631
|
const i = L(st)
|
|
562
632
|
const rows = secretRows(st, term.t)
|
|
@@ -659,15 +729,16 @@ async function onConfirmKey (st, key) {
|
|
|
659
729
|
|
|
660
730
|
// Pestañas INTERNAS de una bóveda ya elegida: se cambian con ←→. La lista de
|
|
661
731
|
// bóvedas (profiles) es el nivel de arriba (se entra con Enter, no es una pestaña).
|
|
662
|
-
const INNER_TABS = ['devices', 'secrets']
|
|
663
|
-
const tabLabel = (i, k) => (
|
|
732
|
+
const INNER_TABS = ['devices', 'secrets', 'me']
|
|
733
|
+
const tabLabel = (i, k) => ({ devices: i.tabDevices, secrets: i.tabSecrets, me: i.tabMe })[k]
|
|
664
734
|
|
|
665
735
|
const helpSegs = (i, screen) => ({
|
|
666
736
|
profiles: i.helpProfiles,
|
|
667
737
|
devices: i.helpDevices,
|
|
668
738
|
secrets: i.helpSecrets,
|
|
669
739
|
pairing: i.helpPairing,
|
|
670
|
-
pairmode: i.helpPairMode
|
|
740
|
+
pairmode: i.helpPairMode,
|
|
741
|
+
me: i.helpMe
|
|
671
742
|
})[screen] || []
|
|
672
743
|
|
|
673
744
|
const title = (i, screen) => ({
|
|
@@ -758,6 +829,7 @@ function render (term, st) {
|
|
|
758
829
|
if (st.screen === 'profiles') body = renderList(profileRows(st, t), st.sel.profiles, contentH, cols, t, scrollRef)
|
|
759
830
|
else if (st.screen === 'devices') body = renderList(deviceRows(st, t), st.sel.devices, contentH, cols, t, scrollRef)
|
|
760
831
|
else if (st.screen === 'secrets') body = renderList(secretRows(st, t), st.sel.secrets, contentH, cols, t, scrollRef)
|
|
832
|
+
else if (st.screen === 'me') body = renderList(meRows(st, t), -1, contentH, cols, t, scrollRef)
|
|
761
833
|
else if (st.screen === 'pairmode') body = renderList(pairModeRows(st, t), st.sel.pairmode, contentH, cols, t, scrollRef)
|
|
762
834
|
else if (st.screen === 'pairing') {
|
|
763
835
|
const pb = pairingBody(st, t, cols, contentH)
|
|
@@ -896,6 +968,9 @@ export async function runTui () {
|
|
|
896
968
|
if ((key.name === 'left' || key.name === 'right') && INNER_TABS.includes(st.screen)) {
|
|
897
969
|
const n = INNER_TABS.indexOf(st.screen)
|
|
898
970
|
st.screen = INNER_TABS[(n + (key.name === 'right' ? 1 : -1) + INNER_TABS.length) % INNER_TABS.length]
|
|
971
|
+
// El perfil se pide al ENTRAR en su pestaña, no al arrancar: es contenido del
|
|
972
|
+
// usuario y no hay por qué sacarlo del cifrado si nadie lo está mirando.
|
|
973
|
+
if (st.screen === 'me' && st.me === undefined) await refreshMe(term, st)
|
|
899
974
|
continue
|
|
900
975
|
}
|
|
901
976
|
// Esc/'b' desde una pestaña vuelve a la lista de bóvedas (salir de la bóveda
|
|
@@ -907,6 +982,7 @@ export async function runTui () {
|
|
|
907
982
|
if (st.screen === 'profiles') running = await onKeyProfiles(term, st, key)
|
|
908
983
|
else if (st.screen === 'devices') running = await onKeyDevices(term, st, key)
|
|
909
984
|
else if (st.screen === 'secrets') running = await onKeySecrets(term, st, key)
|
|
985
|
+
else if (st.screen === 'me') running = await onKeyMe(term, st, key)
|
|
910
986
|
else if (st.screen === 'pairmode') running = await onKeyPairMode(term, st, key)
|
|
911
987
|
else if (st.screen === 'pairing') running = await onKeyPairing(term, st, key)
|
|
912
988
|
}
|
|
@@ -916,4 +992,4 @@ export async function runTui () {
|
|
|
916
992
|
}
|
|
917
993
|
|
|
918
994
|
// Solo para pruebas headless (render sin terminal real). No usar en runtime.
|
|
919
|
-
export const __test = { render, profileRows, deviceRows, secretRows, pairModeRows, pairingBody, scrollBody, fitHelp, toggleLang }
|
|
995
|
+
export const __test = { render, profileRows, deviceRows, secretRows, meRows, pairModeRows, pairingBody, scrollBody, fitHelp, toggleLang }
|
package/src/tui/i18n.js
CHANGED
|
@@ -34,6 +34,31 @@ const es = {
|
|
|
34
34
|
// pestañas y títulos
|
|
35
35
|
tabDevices: 'Dispositivos',
|
|
36
36
|
tabSecrets: 'Scopes y variables',
|
|
37
|
+
tabMe: 'Perfil',
|
|
38
|
+
// Perfil del usuario (lo que sincronizan los dispositivos). Solo lectura: se edita en
|
|
39
|
+
// el aparato, no en la máquina donde vive la bóveda.
|
|
40
|
+
loadingProfile: 'Cargando el perfil…',
|
|
41
|
+
noProfile: ' (esta bóveda todavía no tiene perfil)',
|
|
42
|
+
noProfileHint: ' Edita tu nombre o tu foto en un dispositivo emparejado y pulsa R.',
|
|
43
|
+
profileUpdated: (cuando) => ` actualizado ${cuando}`,
|
|
44
|
+
fieldName: 'nombre',
|
|
45
|
+
fieldPhoto: 'foto',
|
|
46
|
+
fieldFirstName: 'nombres',
|
|
47
|
+
fieldLastName: 'apellidos',
|
|
48
|
+
fieldEmail: 'correo',
|
|
49
|
+
fieldPhone: 'teléfono',
|
|
50
|
+
fieldAddress: 'dirección',
|
|
51
|
+
links: 'Enlaces',
|
|
52
|
+
otherData: 'Otros datos',
|
|
53
|
+
hidden: ' (oculto)',
|
|
54
|
+
noName: '(sin nombre)',
|
|
55
|
+
no: 'no',
|
|
56
|
+
savePhotoHint: ' Pulsa F para guardar la foto y poder verla.',
|
|
57
|
+
savePhotoLabel: '¿Dónde guardo la foto?',
|
|
58
|
+
savePhotoHintInput: 'ruta del archivo (Esc cancela)',
|
|
59
|
+
savingPhoto: 'Guardando la foto…',
|
|
60
|
+
photoSaved: (ruta) => `Foto guardada en ${ruta}`,
|
|
61
|
+
helpMe: ['←→ pestaña', 'r refrescar', 'f guardar foto', 'Esc bóvedas', 'l English', 'q salir'],
|
|
37
62
|
tabsHint: ' (←→ cambiar)',
|
|
38
63
|
titleProfiles: 'Bóvedas',
|
|
39
64
|
titlePairing: 'Emparejar un dispositivo',
|
|
@@ -203,6 +228,29 @@ const en = {
|
|
|
203
228
|
|
|
204
229
|
tabDevices: 'Devices',
|
|
205
230
|
tabSecrets: 'Scopes & variables',
|
|
231
|
+
tabMe: 'Profile',
|
|
232
|
+
loadingProfile: 'Loading profile…',
|
|
233
|
+
noProfile: ' (this vault has no profile yet)',
|
|
234
|
+
noProfileHint: ' Edit your name or photo on a paired device and press R.',
|
|
235
|
+
profileUpdated: (cuando) => ` updated ${cuando}`,
|
|
236
|
+
fieldName: 'name',
|
|
237
|
+
fieldPhoto: 'photo',
|
|
238
|
+
fieldFirstName: 'first name',
|
|
239
|
+
fieldLastName: 'last name',
|
|
240
|
+
fieldEmail: 'email',
|
|
241
|
+
fieldPhone: 'phone',
|
|
242
|
+
fieldAddress: 'address',
|
|
243
|
+
links: 'Links',
|
|
244
|
+
otherData: 'Other data',
|
|
245
|
+
hidden: ' (hidden)',
|
|
246
|
+
noName: '(no name)',
|
|
247
|
+
no: 'no',
|
|
248
|
+
savePhotoHint: ' Press F to save the photo so you can look at it.',
|
|
249
|
+
savePhotoLabel: 'Where should I save the photo?',
|
|
250
|
+
savePhotoHintInput: 'file path (Esc cancels)',
|
|
251
|
+
savingPhoto: 'Saving the photo…',
|
|
252
|
+
photoSaved: (ruta) => `Photo saved to ${ruta}`,
|
|
253
|
+
helpMe: ['←→ tab', 'r refresh', 'f save photo', 'Esc vaults', 'l Español', 'q quit'],
|
|
206
254
|
tabsHint: ' (←→ switch)',
|
|
207
255
|
titleProfiles: 'Vaults',
|
|
208
256
|
titlePairing: 'Pair a device',
|
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 } })
|
package/src/vaultControl.js
CHANGED
|
@@ -32,6 +32,7 @@ const F = {
|
|
|
32
32
|
devices: 'devices.json',
|
|
33
33
|
profilesList: 'profiles-list.json',
|
|
34
34
|
secretsList: 'secrets-list.json',
|
|
35
|
+
me: 'me.json',
|
|
35
36
|
// peticiones (las escribe el control; el daemon las consume y borra)
|
|
36
37
|
pairReq: 'pair-request.json',
|
|
37
38
|
approveReq: 'approve-request.json',
|
|
@@ -39,6 +40,7 @@ const F = {
|
|
|
39
40
|
revokeReq: 'revoke-request.json',
|
|
40
41
|
secretReq: 'secret-request.json',
|
|
41
42
|
profileReq: 'profile-request.json',
|
|
43
|
+
meReq: 'me-request.json',
|
|
42
44
|
dumpReq: 'dump-request.json'
|
|
43
45
|
}
|
|
44
46
|
|
|
@@ -206,6 +208,44 @@ export async function revokeDevice (nonce, profile) {
|
|
|
206
208
|
return listDevices(profile)
|
|
207
209
|
}
|
|
208
210
|
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Perfil del usuario (lo que sincronizan los dispositivos)
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* El PERFIL del usuario tal como lo tiene la bóveda: nombre, foto y datos. Es lo que se
|
|
217
|
+
* edita en cualquier dispositivo emparejado y se sincroniza aquí, así que sirve para
|
|
218
|
+
* comprobar que lo que cambiaste en el aparato llegó.
|
|
219
|
+
*
|
|
220
|
+
* No es lo mismo que `listProfiles` (las cuentas de ESTE PC) ni que el acta (quién es del
|
|
221
|
+
* perfil): esto es el CONTENIDO. La foto llega resumida (tipo y tamaño), no en bytes:
|
|
222
|
+
* nadie va a mirar un data-URI de 90 KB en una terminal.
|
|
223
|
+
*
|
|
224
|
+
* El volcado es contenido del usuario, así que se lee y se BORRA en el acto.
|
|
225
|
+
*/
|
|
226
|
+
export async function getMe (profile) {
|
|
227
|
+
requireAlive()
|
|
228
|
+
rm(F.me)
|
|
229
|
+
writeReq(F.meReq, {}, profile)
|
|
230
|
+
signalOrCleanup('SIGUSR2', [F.meReq])
|
|
231
|
+
const d = await waitFor(F.me)
|
|
232
|
+
rm(F.me)
|
|
233
|
+
if (!d) throw coded('the daemon did not reply', 'NO_REPLY')
|
|
234
|
+
return d.me || null
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Escribe la foto de perfil en `destino` (para poder mirarla) y devuelve la ruta. */
|
|
238
|
+
export async function saveAvatar (destino, profile) {
|
|
239
|
+
requireAlive()
|
|
240
|
+
rm(F.me)
|
|
241
|
+
writeReq(F.meReq, { avatarPath: destino }, profile)
|
|
242
|
+
signalOrCleanup('SIGUSR2', [F.meReq])
|
|
243
|
+
const d = await waitFor(F.me)
|
|
244
|
+
rm(F.me)
|
|
245
|
+
if (!d) throw coded('the daemon did not reply', 'NO_REPLY')
|
|
246
|
+
return d.avatarGuardada || null
|
|
247
|
+
}
|
|
248
|
+
|
|
209
249
|
// ---------------------------------------------------------------------------
|
|
210
250
|
// Secretos: scopes (namespaces) y variables (claves)
|
|
211
251
|
// ---------------------------------------------------------------------------
|