@dotrino/vaultd 0.7.3 → 0.7.4
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 +6 -3
- package/package.json +1 -1
- package/src/ctl.js +17 -0
- package/src/daemon.js +3 -1
- package/src/tui/app.js +83 -7
- package/src/tui/i18n.js +25 -1
package/README.md
CHANGED
|
@@ -103,9 +103,12 @@ los dispositivos/variables que estás viendo:
|
|
|
103
103
|
**entrar** a uno — lo activa si no lo estaba). Ahí también creas una bóveda
|
|
104
104
|
nueva, renombras, borras y pones/quitas/usas la contraseña (candado).
|
|
105
105
|
2. Al entrar caes en sus **pestañas horizontales**, que cambias con `←→`:
|
|
106
|
-
- **Dispositivos (pares):** verlos, **emparejar** uno nuevo
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
- **Dispositivos (pares):** verlos, **emparejar** uno nuevo, **aprobar** con el
|
|
107
|
+
código que muestra el dispositivo, **rechazar** y **revocar**. Al emparejar, la
|
|
108
|
+
bóveda **pregunta primero a qué cuenta entra el dispositivo** —a esta, o a una
|
|
109
|
+
cuenta nueva que se estrena para él— y recién después muestra el QR, que además
|
|
110
|
+
dice de qué cuenta salió. (En la CLI: `dotrino-vault pair --new-account
|
|
111
|
+
[nombre]`.)
|
|
109
112
|
- **Scopes y variables (secretos):** ver los scopes y sus variables (nunca los
|
|
110
113
|
valores), **agregar** una variable (con su scope) y **quitar** una variable o
|
|
111
114
|
un scope entero.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
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
|
@@ -153,6 +153,20 @@ async function cmdPair (args = []) {
|
|
|
153
153
|
console.error('uso: dotrino-vault pair --service <ns> (ns en minúsculas, p.ej. proxy)'); process.exit(2)
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
+
// `--new-account [nombre]`: la otra respuesta a «¿a qué cuenta entra?». En vez de
|
|
157
|
+
// meter el dispositivo en una cuenta que ya vive aquí, se ESTRENA una (vacía) y
|
|
158
|
+
// entra a ella; las demás no se tocan. En la TUI esto es una pregunta con sus
|
|
159
|
+
// opciones; en la CLI es una bandera, para que siga sirviendo en un script.
|
|
160
|
+
const naIdx = args.findIndex((a) => a === '--new-account')
|
|
161
|
+
if (naIdx >= 0) {
|
|
162
|
+
const next = args[naIdx + 1]
|
|
163
|
+
const name = (next && !next.startsWith('-')) ? next : `cuenta ${new Date().toISOString().slice(0, 10)}`
|
|
164
|
+
const d = await profileRequest('add', { name })
|
|
165
|
+
if (d.error) { console.error('%s', d.error); process.exit(1) }
|
|
166
|
+
if (!d.id) { console.error('El daemon no dijo qué cuenta creó.'); process.exit(1) }
|
|
167
|
+
PROFILE = d.id // el emparejamiento y los comandos siguientes apuntan a ELLA
|
|
168
|
+
console.log('Cuenta nueva: %s (%s)', name, d.id)
|
|
169
|
+
}
|
|
156
170
|
// La petición se escribe SIEMPRE (aunque no haya --service): lleva a qué perfil
|
|
157
171
|
// se empareja el dispositivo.
|
|
158
172
|
writeReq('pair-request.json', service ? { service } : {})
|
|
@@ -539,6 +553,9 @@ function help () {
|
|
|
539
553
|
tui interfaz de terminal a pantalla completa (bóvedas, pares, secretos)
|
|
540
554
|
status estado del servicio + fingerprint
|
|
541
555
|
pair [--save <f>] inicia un emparejamiento (QR + espera); --save escribe la invitación (.dpair)
|
|
556
|
+
pair --new-account [nombre]
|
|
557
|
+
estrena una cuenta VACÍA en este vault y mete ahí al dispositivo
|
|
558
|
+
(sin la bandera entra a la cuenta activa, o a la de --profile)
|
|
542
559
|
pair --service <ns> empareja un SERVICIO (proxy, geo…) con acceso SOLO a sus secretos
|
|
543
560
|
secret set <ns> <CLAVE> <valor> guarda un secreto para el servicio <ns>
|
|
544
561
|
secret rm <ns> <CLAVE> borra un secreto
|
package/src/daemon.js
CHANGED
|
@@ -156,7 +156,9 @@ export async function runDaemon () {
|
|
|
156
156
|
const ref = () => mgr.resolve(req.profile || mgr.currentId())
|
|
157
157
|
switch (req.op) {
|
|
158
158
|
case 'list': return {} // el volcado de perfiles ya se hace abajo
|
|
159
|
-
|
|
159
|
+
// `id`: quien la crea necesita saber CUÁL quedó, no adivinar por nombre (dos
|
|
160
|
+
// cuentas pueden llamarse igual). Lo usa «emparejar en una cuenta nueva».
|
|
161
|
+
case 'add': { const p = await mgr.add(req.name); return { done: `perfil creado: ${p.name || p.id}`, id: p.id } }
|
|
160
162
|
case 'rm': { const r = await mgr.remove(req.profile); return { done: `perfil borrado: ${r.name || r.id}` } }
|
|
161
163
|
case 'rename': { const p = mgr.profiles.rename(ref(), req.name); return { done: `perfil renombrado: ${p.name}` } }
|
|
162
164
|
case 'use': { const p = mgr.profiles.setCurrent(ref()); return { done: `perfil activo: ${p.name || p.id}` } }
|
package/src/tui/app.js
CHANGED
|
@@ -174,6 +174,30 @@ function deviceRows (st, t) {
|
|
|
174
174
|
return rows
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* LA PREGUNTA DEL EMPAREJAMIENTO. La decisión es del vault (es quien lo inicia) y
|
|
179
|
+
* este daemon puede tener varias cuentas: antes de mostrar el QR hay que decir a
|
|
180
|
+
* cuál entra el dispositivo. Hoy se responde con las dos formas que existen —una
|
|
181
|
+
* cuenta que ya vive aquí, o una nueva que se estrena para él—; la tercera
|
|
182
|
+
* («adoptar la que trae el aparato») necesita el protocolo de adopción y se
|
|
183
|
+
* muestra desactivada para no prometer lo que todavía no hace
|
|
184
|
+
* (docs/vinculacion-de-cuentas.md §5).
|
|
185
|
+
*/
|
|
186
|
+
function pairModeRows (st, t) {
|
|
187
|
+
const i = L(st)
|
|
188
|
+
const ap = activeProfile(st)
|
|
189
|
+
const rows = [{ text: t.muted(' ' + i.pairModeIntro), sel: false }, { text: '', sel: false }]
|
|
190
|
+
rows.push({ text: ` ${t.bold(i.pairModeHere(ap?.name || ap?.id || '—'))}`, sel: true, meta: { mode: 'here' } })
|
|
191
|
+
rows.push({ text: t.muted(' ' + i.pairModeHereHint), sel: false })
|
|
192
|
+
rows.push({ text: '', sel: false })
|
|
193
|
+
rows.push({ text: ` ${t.bold(i.pairModeNew)}`, sel: true, meta: { mode: 'new' } })
|
|
194
|
+
rows.push({ text: t.muted(' ' + i.pairModeNewHint), sel: false })
|
|
195
|
+
rows.push({ text: '', sel: false })
|
|
196
|
+
rows.push({ text: ' ' + t.muted(i.pairModeAdopt), sel: false })
|
|
197
|
+
rows.push({ text: t.muted(' (' + i.pairModeAdoptSoon + ')'), sel: false })
|
|
198
|
+
return rows
|
|
199
|
+
}
|
|
200
|
+
|
|
177
201
|
function secretRows (st, t) {
|
|
178
202
|
const i = L(st)
|
|
179
203
|
const ns = st.secrets || {}
|
|
@@ -381,9 +405,10 @@ async function onKeyDevices (term, st, key) {
|
|
|
381
405
|
const cur = sels[Math.min(st.sel.devices, sels.length - 1)]
|
|
382
406
|
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
383
407
|
|
|
384
|
-
if (ch === 'p') { // pair
|
|
385
|
-
|
|
386
|
-
|
|
408
|
+
if (ch === 'p') { // pair → primero LA PREGUNTA (a qué cuenta entra), luego el QR
|
|
409
|
+
st.sel.pairmode = 0
|
|
410
|
+
st.scroll.pairmode = { value: 0 }
|
|
411
|
+
st.screen = 'pairmode'
|
|
387
412
|
} else if (ch === 'a') { // aprobar el pendiente
|
|
388
413
|
if (!st.pending) { flash(st, i.noPending, 'warn'); return true }
|
|
389
414
|
promptApprove(term, st)
|
|
@@ -407,6 +432,50 @@ async function onKeyDevices (term, st, key) {
|
|
|
407
432
|
return true
|
|
408
433
|
}
|
|
409
434
|
|
|
435
|
+
/** Abre el emparejamiento contra `profile` y salta a la pantalla del QR. */
|
|
436
|
+
async function beginPairing (term, st, profile) {
|
|
437
|
+
const r = await guard(term, st, L(st).startingPairing, () => vc.startPairing({ profile }))
|
|
438
|
+
if (r.ok) { st.pairing = r.v; st.pending = null; st.screen = 'pairing' }
|
|
439
|
+
return r.ok
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function onKeyPairMode (term, st, key) {
|
|
443
|
+
const i = L(st)
|
|
444
|
+
const rows = pairModeRows(st, term.t)
|
|
445
|
+
const sels = rows.filter((r) => r.sel).map((r) => r.meta)
|
|
446
|
+
moveSel(st, key, 'pairmode', sels.length)
|
|
447
|
+
const cur = sels[Math.min(st.sel.pairmode, sels.length - 1)]
|
|
448
|
+
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
449
|
+
|
|
450
|
+
if (key.name === 'escape' || ch === 'b') { st.screen = 'devices'; return true }
|
|
451
|
+
if (key.name !== 'enter' || !cur) return true
|
|
452
|
+
|
|
453
|
+
if (cur.mode === 'here') { await beginPairing(term, st, activeId(st)); return true }
|
|
454
|
+
|
|
455
|
+
// Cuenta nueva: se crea aquí, se ACTIVA (así aprobar/rechazar y las listas miran
|
|
456
|
+
// a la misma que el QR) y recién entonces se abre el emparejamiento contra ella.
|
|
457
|
+
setInput(st, {
|
|
458
|
+
label: i.newAccountLabel,
|
|
459
|
+
hint: i.newAccountHint,
|
|
460
|
+
onSubmit: async (name) => {
|
|
461
|
+
st.input = null
|
|
462
|
+
const nombre = name.trim()
|
|
463
|
+
if (!nombre) { flash(st, i.nameEmpty, 'danger'); return }
|
|
464
|
+
const r = await guard(term, st, i.creatingVault, () => vc.addProfile(nombre))
|
|
465
|
+
if (!r.ok) return
|
|
466
|
+
const nuevo = r.v?.id || (r.v?.profiles || []).find((p) => p.name === nombre)?.id
|
|
467
|
+
if (!nuevo) { flash(st, i.errNoReply, 'danger'); return }
|
|
468
|
+
const u = await guard(term, st, i.switchingVault, () => vc.useProfile(nuevo))
|
|
469
|
+
if (!u.ok) return
|
|
470
|
+
await refreshAll(term, st)
|
|
471
|
+
flash(st, i.accountCreated(nombre))
|
|
472
|
+
await beginPairing(term, st, nuevo)
|
|
473
|
+
},
|
|
474
|
+
onCancel: () => { st.input = null }
|
|
475
|
+
})
|
|
476
|
+
return true
|
|
477
|
+
}
|
|
478
|
+
|
|
410
479
|
function promptApprove (term, st) {
|
|
411
480
|
const i = L(st)
|
|
412
481
|
setInput(st, {
|
|
@@ -554,10 +623,15 @@ const helpSegs = (i, screen) => ({
|
|
|
554
623
|
profiles: i.helpProfiles,
|
|
555
624
|
devices: i.helpDevices,
|
|
556
625
|
secrets: i.helpSecrets,
|
|
557
|
-
pairing: i.helpPairing
|
|
626
|
+
pairing: i.helpPairing,
|
|
627
|
+
pairmode: i.helpPairMode
|
|
558
628
|
})[screen] || []
|
|
559
629
|
|
|
560
|
-
const title = (i, screen) => (
|
|
630
|
+
const title = (i, screen) => ({
|
|
631
|
+
profiles: i.titleProfiles,
|
|
632
|
+
pairing: i.titlePairing,
|
|
633
|
+
pairmode: i.titlePairMode
|
|
634
|
+
})[screen] || ''
|
|
561
635
|
|
|
562
636
|
/** Barra de pestañas horizontal (Dispositivos | Scopes y variables) de la bóveda entrada. */
|
|
563
637
|
function renderTabs (st, t) {
|
|
@@ -639,6 +713,7 @@ function render (term, st) {
|
|
|
639
713
|
if (st.screen === 'profiles') body = renderList(profileRows(st, t), st.sel.profiles, contentH, cols, t, scrollRef)
|
|
640
714
|
else if (st.screen === 'devices') body = renderList(deviceRows(st, t), st.sel.devices, contentH, cols, t, scrollRef)
|
|
641
715
|
else if (st.screen === 'secrets') body = renderList(secretRows(st, t), st.sel.secrets, contentH, cols, t, scrollRef)
|
|
716
|
+
else if (st.screen === 'pairmode') body = renderList(pairModeRows(st, t), st.sel.pairmode, contentH, cols, t, scrollRef)
|
|
642
717
|
else if (st.screen === 'pairing') {
|
|
643
718
|
const pb = pairingBody(st, t, cols, contentH)
|
|
644
719
|
body = pb.slice(0, contentH)
|
|
@@ -724,7 +799,7 @@ export async function runTui () {
|
|
|
724
799
|
const st = {
|
|
725
800
|
screen: 'profiles', // se arranca en la lista de bóvedas: hay que ENTRAR a una
|
|
726
801
|
lang: loadLang(), // es/en — se conmuta con `l` y se recuerda en prefs.json
|
|
727
|
-
sel: { profiles: 0, devices: 0, secrets: 0 },
|
|
802
|
+
sel: { profiles: 0, devices: 0, secrets: 0, pairmode: 0 },
|
|
728
803
|
scroll: {},
|
|
729
804
|
profiles: null,
|
|
730
805
|
devices: null,
|
|
@@ -788,6 +863,7 @@ export async function runTui () {
|
|
|
788
863
|
if (st.screen === 'profiles') running = await onKeyProfiles(term, st, key)
|
|
789
864
|
else if (st.screen === 'devices') running = await onKeyDevices(term, st, key)
|
|
790
865
|
else if (st.screen === 'secrets') running = await onKeySecrets(term, st, key)
|
|
866
|
+
else if (st.screen === 'pairmode') running = await onKeyPairMode(term, st, key)
|
|
791
867
|
else if (st.screen === 'pairing') running = await onKeyPairing(term, st, key)
|
|
792
868
|
}
|
|
793
869
|
} finally {
|
|
@@ -796,4 +872,4 @@ export async function runTui () {
|
|
|
796
872
|
}
|
|
797
873
|
|
|
798
874
|
// Solo para pruebas headless (render sin terminal real). No usar en runtime.
|
|
799
|
-
export const __test = { render, profileRows, deviceRows, secretRows, pairingBody, fitHelp, toggleLang }
|
|
875
|
+
export const __test = { render, profileRows, deviceRows, secretRows, pairModeRows, pairingBody, fitHelp, toggleLang }
|
package/src/tui/i18n.js
CHANGED
|
@@ -37,6 +37,7 @@ const es = {
|
|
|
37
37
|
tabsHint: ' (←→ cambiar)',
|
|
38
38
|
titleProfiles: 'Bóvedas',
|
|
39
39
|
titlePairing: 'Emparejar un dispositivo',
|
|
40
|
+
titlePairMode: 'Emparejar: ¿a qué cuenta entra?',
|
|
40
41
|
|
|
41
42
|
// bóvedas (perfiles)
|
|
42
43
|
noPassword: 'sin clave',
|
|
@@ -122,7 +123,17 @@ const es = {
|
|
|
122
123
|
savingVar: 'Guardando variable…',
|
|
123
124
|
varSaved: (ns, key) => `Guardado ${ns}/${key}`,
|
|
124
125
|
|
|
125
|
-
// emparejamiento
|
|
126
|
+
// emparejamiento — la PREGUNTA es del vault, que es quien lo inicia
|
|
127
|
+
pairModeIntro: 'Un dispositivo puede entrar a una cuenta que ya vive aquí, o estrenar una.',
|
|
128
|
+
pairModeHere: (name) => `Entrar a esta cuenta: ${name}`,
|
|
129
|
+
pairModeHereHint: 'el dispositivo pasa a ver y firmar lo de esta cuenta',
|
|
130
|
+
pairModeNew: 'Estrenar una cuenta nueva en este vault',
|
|
131
|
+
pairModeNewHint: 'se crea aquí, vacía, y el dispositivo entra a ELLA (las otras no se tocan)',
|
|
132
|
+
pairModeAdopt: 'Adoptar la cuenta que trae el dispositivo',
|
|
133
|
+
pairModeAdoptSoon: 'todavía no: el dispositivo aún no sabe entregar la suya',
|
|
134
|
+
newAccountLabel: 'Nombre de la cuenta nueva',
|
|
135
|
+
newAccountHint: 'nace vacía; el dispositivo será su primer invitado',
|
|
136
|
+
accountCreated: (name) => `Cuenta creada: ${name}`,
|
|
126
137
|
pairAccount: (name) => `Cuenta que se comparte: ${name}`,
|
|
127
138
|
pairValid: (min) => `Válido ~${min} min. Escanéalo o abre la URL en el dispositivo.`,
|
|
128
139
|
pairUrl: 'URL: ',
|
|
@@ -144,6 +155,7 @@ const es = {
|
|
|
144
155
|
helpDevices: ['←→ pestaña', '↑↓', 'p emparejar', 'a aprobar', 'x rechazar', 'v revocar', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
|
|
145
156
|
helpSecrets: ['←→ pestaña', '↑↓', 'n nueva variable', 'x quitar (variable/scope)', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
|
|
146
157
|
helpPairing: ['a aprobar', 'x rechazar', 'r reiniciar', 'Esc atrás', 'l English'],
|
|
158
|
+
helpPairMode: ['↑↓', 'Enter elegir', 'Esc atrás', 'l English', 'q salir'],
|
|
147
159
|
|
|
148
160
|
// pantalla "daemon caído"
|
|
149
161
|
downTitle: 'El daemon del vault no está corriendo.',
|
|
@@ -190,6 +202,7 @@ const en = {
|
|
|
190
202
|
tabsHint: ' (←→ switch)',
|
|
191
203
|
titleProfiles: 'Vaults',
|
|
192
204
|
titlePairing: 'Pair a device',
|
|
205
|
+
titlePairMode: 'Pairing: which account does it join?',
|
|
193
206
|
|
|
194
207
|
noPassword: 'no password',
|
|
195
208
|
locked: '🔒 locked',
|
|
@@ -272,6 +285,16 @@ const en = {
|
|
|
272
285
|
savingVar: 'Saving variable…',
|
|
273
286
|
varSaved: (ns, key) => `Saved ${ns}/${key}`,
|
|
274
287
|
|
|
288
|
+
pairModeIntro: 'A device can join an account that already lives here, or start a new one.',
|
|
289
|
+
pairModeHere: (name) => `Join this account: ${name}`,
|
|
290
|
+
pairModeHereHint: 'the device gets to see and sign for this account',
|
|
291
|
+
pairModeNew: 'Start a new account in this vault',
|
|
292
|
+
pairModeNewHint: 'created here, empty, and the device joins THAT one (the others are untouched)',
|
|
293
|
+
pairModeAdopt: 'Adopt the account the device brings',
|
|
294
|
+
pairModeAdoptSoon: 'not yet: the device cannot hand its own over',
|
|
295
|
+
newAccountLabel: 'Name of the new account',
|
|
296
|
+
newAccountHint: 'born empty; the device will be its first guest',
|
|
297
|
+
accountCreated: (name) => `Account created: ${name}`,
|
|
275
298
|
pairAccount: (name) => `Account being shared: ${name}`,
|
|
276
299
|
pairValid: (min) => `Valid ~${min} min. Scan it or open the URL on the device.`,
|
|
277
300
|
pairUrl: 'URL: ',
|
|
@@ -288,6 +311,7 @@ const en = {
|
|
|
288
311
|
helpDevices: ['←→ tab', '↑↓', 'p pair', 'a approve', 'x reject', 'v revoke', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
|
|
289
312
|
helpSecrets: ['←→ tab', '↑↓', 'n new variable', 'x remove (variable/scope)', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
|
|
290
313
|
helpPairing: ['a approve', 'x reject', 'r restart', 'Esc back', 'l Español'],
|
|
314
|
+
helpPairMode: ['↑↓', 'Enter choose', 'Esc back', 'l Español', 'q quit'],
|
|
291
315
|
|
|
292
316
|
downTitle: 'The vault daemon is not running.',
|
|
293
317
|
downBody1: 'The TUI gives orders to the daemon (the keeper of your key). Without it',
|