@dotrino/vaultd 0.7.5 → 0.7.7
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 +1 -1
- package/src/tui/app.js +56 -12
- package/src/tui/i18n.js +6 -4
- package/src/vaultControl.js +3 -3
package/README.md
CHANGED
|
@@ -100,8 +100,10 @@ raw mode).
|
|
|
100
100
|
los dispositivos/variables que estás viendo:
|
|
101
101
|
|
|
102
102
|
1. **Bóvedas** es la pantalla de entrada: lista tus perfiles (`↑↓` mover, `Enter`
|
|
103
|
-
**entrar** a uno — lo activa si no lo estaba). Ahí también
|
|
104
|
-
|
|
103
|
+
**entrar** a uno — lo activa si no lo estaba). Ahí también **conectas un
|
|
104
|
+
dispositivo** con `p` (sin entrar: activa la bóveda elegida y abre la pregunta
|
|
105
|
+
de a qué cuenta entra), creas una bóveda nueva, renombras, borras y
|
|
106
|
+
pones/quitas/usas la contraseña (candado).
|
|
105
107
|
2. Al entrar caes en sus **pestañas horizontales**, que cambias con `←→`:
|
|
106
108
|
- **Dispositivos (pares):** verlos, **emparejar** uno nuevo, **aprobar** con el
|
|
107
109
|
código que muestra el dispositivo, **rechazar** y **revocar**. Al emparejar, la
|
|
@@ -132,7 +134,8 @@ en español (solo se traduce la palabra que las explica en la barra de ayuda).
|
|
|
132
134
|
| `n` | new — bóveda nueva / variable nueva | Bóvedas · Scopes |
|
|
133
135
|
| `r` | rename (Bóvedas) · refresh (Dispositivos/Scopes) · restart (Emparejar) | — |
|
|
134
136
|
| `d` | delete — borrar la bóveda | Bóvedas |
|
|
135
|
-
| `p` |
|
|
137
|
+
| `p` | **pair — conectar un dispositivo** (desde Bóvedas entra directo, sin `Enter`) | Bóvedas · Dispositivos |
|
|
138
|
+
| `c` | change password — poner/cambiar la contraseña | Bóvedas |
|
|
136
139
|
| `x` | quitar: contraseña · dispositivo pendiente · variable/scope | todas |
|
|
137
140
|
| `u` / `k` | unlock / locK — candado de la bóveda | Bóvedas |
|
|
138
141
|
| `a` | approve — aprobar el dispositivo | Dispositivos · Emparejar |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
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
|
@@ -190,7 +190,7 @@ async function cmdPair (args = []) {
|
|
|
190
190
|
console.log(`${R}${B}⚠ Este código deja LEER tus datos y FIRMAR con tu identidad.${Z}`)
|
|
191
191
|
console.log(`${R} NO lo compartas con nadie, ni con "soporte". Solo escanéalo en TU dispositivo.${Z}`)
|
|
192
192
|
console.log('\nO abre esta dirección en el dispositivo:\n ' + url)
|
|
193
|
-
console.log('\nO pega este código en vault.dotrino.com/dispositivos :\n ' +
|
|
193
|
+
console.log('\nO pega este código en vault.dotrino.com/dispositivos :\n ' + b64)
|
|
194
194
|
|
|
195
195
|
// --save [archivo]: escribe la invitación (.dpair) para transferirla y abrirla en profile.
|
|
196
196
|
const saveIdx = args.indexOf('--save')
|
package/src/tui/app.js
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
* LAS TECLAS NO CAMBIAN CON EL IDIOMA: son mnemónicos en INGLÉS y son las mismas
|
|
19
19
|
* en español (lo que se traduce es la palabra que las explica en la barra de
|
|
20
20
|
* ayuda). new · rename · delete · password · unlock · locK · pair · approve ·
|
|
21
|
-
* reject · reVoke · refresh · back · language · quit.
|
|
22
|
-
*
|
|
21
|
+
* reject · reVoke · refresh · back · language · quit. Una tecla significa LO MISMO
|
|
22
|
+
* en todas las pantallas: `p` es SIEMPRE emparejar (también en Bóvedas, sin tener
|
|
23
|
+
* que entrar antes), el candado es `k` (la `l` es el idioma) y la contraseña `c`.
|
|
23
24
|
*/
|
|
24
25
|
import { execFile } from 'node:child_process'
|
|
25
26
|
import { createTerm, widthOf } from './term.js'
|
|
@@ -117,6 +118,19 @@ function renderList (rows, selIdx, height, cols, t, scrollRef) {
|
|
|
117
118
|
return out
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Scroll simple para una lista de líneas planas (no seleccionables). Igual idea
|
|
123
|
+
* que `renderList`, pero sin índice de selección: solo desplaza la ventana.
|
|
124
|
+
*/
|
|
125
|
+
function scrollBody (lines, height, scrollRef) {
|
|
126
|
+
let top = scrollRef.value || 0
|
|
127
|
+
top = Math.max(0, Math.min(top, Math.max(0, lines.length - height)))
|
|
128
|
+
scrollRef.value = top
|
|
129
|
+
const out = []
|
|
130
|
+
for (let i = 0; i < height; i++) out.push(lines[top + i] ?? '')
|
|
131
|
+
return out
|
|
132
|
+
}
|
|
133
|
+
|
|
120
134
|
/**
|
|
121
135
|
* Barra de ayuda que SIEMPRE deja ver lo global (idioma y salir): si los segmentos
|
|
122
136
|
* no caben, recorta desde el MEDIO y marca el corte con «…». Sin esto, en 80
|
|
@@ -318,6 +332,19 @@ async function onKeyProfiles (term, st, key) {
|
|
|
318
332
|
}
|
|
319
333
|
st.screen = 'devices'
|
|
320
334
|
await refreshDevices(term, st)
|
|
335
|
+
} else if (ch === 'p' && cur) {
|
|
336
|
+
// Emparejar SIN tener que entrar antes: `p` significa lo mismo aquí que en la
|
|
337
|
+
// pestaña Dispositivos. Se activa la bóveda elegida (el QR sale de UNA, y las
|
|
338
|
+
// acciones siguientes —aprobar, revocar— miran a la activa) y se abre la
|
|
339
|
+
// pregunta de a qué cuenta entra el dispositivo.
|
|
340
|
+
if (!cur.current) {
|
|
341
|
+
const r = await guard(term, st, i.switchingVault, () => vc.useProfile(cur.id))
|
|
342
|
+
if (!r.ok) return true
|
|
343
|
+
await refreshAll(term, st)
|
|
344
|
+
}
|
|
345
|
+
st.sel.pairmode = 0
|
|
346
|
+
st.scroll.pairmode = { value: 0 }
|
|
347
|
+
st.screen = 'pairmode'
|
|
321
348
|
} else if (ch === 'n') {
|
|
322
349
|
setInput(st, {
|
|
323
350
|
label: i.newVaultLabel,
|
|
@@ -355,7 +382,7 @@ async function onKeyProfiles (term, st, key) {
|
|
|
355
382
|
},
|
|
356
383
|
onCancel: () => { st.input = null }
|
|
357
384
|
}))
|
|
358
|
-
} else if (ch === '
|
|
385
|
+
} else if (ch === 'c' && cur) { // change password (la `p` es emparejar, igual que en Dispositivos)
|
|
359
386
|
await ensureUnlocked(term, st, cur, (p = cur) => setInput(st, {
|
|
360
387
|
label: i.newPasswordLabel(p.name || p.id),
|
|
361
388
|
mask: true,
|
|
@@ -436,7 +463,7 @@ async function onKeyDevices (term, st, key) {
|
|
|
436
463
|
/** Abre el emparejamiento contra `profile` y salta a la pantalla del QR. */
|
|
437
464
|
async function beginPairing (term, st, profile) {
|
|
438
465
|
const r = await guard(term, st, L(st).startingPairing, () => vc.startPairing({ profile }))
|
|
439
|
-
if (r.ok) { st.pairing = r.v; st.pending = null; st.screen = 'pairing' }
|
|
466
|
+
if (r.ok) { st.pairing = r.v; st.pending = null; st.scroll.pairing = { value: 0 }; st.screen = 'pairing' }
|
|
440
467
|
return r.ok
|
|
441
468
|
}
|
|
442
469
|
|
|
@@ -500,6 +527,21 @@ async function onKeyPairing (term, st, key) {
|
|
|
500
527
|
if (pend) st.pending = pend
|
|
501
528
|
return true
|
|
502
529
|
}
|
|
530
|
+
// Scroll vertical para ver el QR completo cuando no cabe en la pantalla.
|
|
531
|
+
if (['up', 'down', 'pageup', 'pagedown', 'home', 'end'].includes(key.name)) {
|
|
532
|
+
const { rows } = term.size()
|
|
533
|
+
const contentH = Math.max(1, rows - 7)
|
|
534
|
+
const pb = pairingBody(st, term.t, term.size().cols, contentH)
|
|
535
|
+
const maxScroll = Math.max(0, pb.length - contentH)
|
|
536
|
+
const scroll = st.scroll.pairing || (st.scroll.pairing = { value: 0 })
|
|
537
|
+
if (key.name === 'up') scroll.value = Math.max(0, scroll.value - 1)
|
|
538
|
+
else if (key.name === 'down') scroll.value = Math.min(maxScroll, scroll.value + 1)
|
|
539
|
+
else if (key.name === 'pageup') scroll.value = Math.max(0, scroll.value - 5)
|
|
540
|
+
else if (key.name === 'pagedown') scroll.value = Math.min(maxScroll, scroll.value + 5)
|
|
541
|
+
else if (key.name === 'home') scroll.value = 0
|
|
542
|
+
else if (key.name === 'end') scroll.value = maxScroll
|
|
543
|
+
return true
|
|
544
|
+
}
|
|
503
545
|
if (ch === 'a' && st.pending) { promptApprove(term, st); return true }
|
|
504
546
|
if (ch === 'x' && st.pending) {
|
|
505
547
|
const r = await guard(term, st, i.rejecting, () => vc.rejectPending(st.pending.deviceId, activeId(st)))
|
|
@@ -508,7 +550,7 @@ async function onKeyPairing (term, st, key) {
|
|
|
508
550
|
}
|
|
509
551
|
if (ch === 'r') { // restart: reiniciar el emparejamiento
|
|
510
552
|
const r = await guard(term, st, i.restartingPairing, () => vc.startPairing({ profile: activeId(st) }))
|
|
511
|
-
if (r.ok) { st.pairing = r.v; st.pending = null }
|
|
553
|
+
if (r.ok) { st.pairing = r.v; st.pending = null; st.scroll.pairing = { value: 0 } }
|
|
512
554
|
return true
|
|
513
555
|
}
|
|
514
556
|
if (key.name === 'escape' || ch === 'b') { st.screen = 'devices'; st.pairing = null; await refreshDevices(term, st) }
|
|
@@ -656,20 +698,23 @@ function pairingBody (st, t, cols, height) {
|
|
|
656
698
|
const left = Math.max(0, Math.round((info.expiresAt - Date.now()) / 60000))
|
|
657
699
|
lines.push(t.muted(i.pairValid(left)))
|
|
658
700
|
lines.push('')
|
|
659
|
-
// QR
|
|
701
|
+
// QR: se dibuja siempre que quepa de ancho; si es más alto que la pantalla se
|
|
702
|
+
// puede hacer scroll hacia arriba/abajo para verlo completo.
|
|
660
703
|
let qr = ''
|
|
661
704
|
try { qr = qrToString(info.url) } catch (_) {}
|
|
662
705
|
const qrLines = qr ? qr.replace(/\n$/, '').split('\n') : []
|
|
663
706
|
const qrWidth = qrLines.length ? Math.max(...qrLines.map((l) => l.replace(/\x1b\[[0-9;]*m/g, '').length)) : 0
|
|
664
|
-
|
|
665
|
-
if (qrLines.length && qrWidth <= cols && qrLines.length <= height - reserved) {
|
|
707
|
+
if (qrLines.length && qrWidth <= cols) {
|
|
666
708
|
for (const l of qrLines) lines.push(l)
|
|
667
709
|
lines.push('')
|
|
710
|
+
} else if (qrLines.length) {
|
|
711
|
+
lines.push(t.warn(i.pairQrTooNarrow(cols, qrWidth)))
|
|
712
|
+
lines.push('')
|
|
668
713
|
}
|
|
669
714
|
lines.push(t.bold(i.pairUrl) + info.url)
|
|
670
715
|
lines.push('')
|
|
671
716
|
lines.push(t.muted(i.pairPaste))
|
|
672
|
-
lines.push(info.payload)
|
|
717
|
+
lines.push(info.b64 || info.payload)
|
|
673
718
|
lines.push('')
|
|
674
719
|
lines.push(t.danger(i.pairWarning))
|
|
675
720
|
lines.push('')
|
|
@@ -717,8 +762,7 @@ function render (term, st) {
|
|
|
717
762
|
else if (st.screen === 'pairmode') body = renderList(pairModeRows(st, t), st.sel.pairmode, contentH, cols, t, scrollRef)
|
|
718
763
|
else if (st.screen === 'pairing') {
|
|
719
764
|
const pb = pairingBody(st, t, cols, contentH)
|
|
720
|
-
body = pb
|
|
721
|
-
while (body.length < contentH) body.push('')
|
|
765
|
+
body = scrollBody(pb, contentH, scrollRef)
|
|
722
766
|
}
|
|
723
767
|
for (let n = 0; n < contentH; n++) lines[top + n] = body[n] ?? ''
|
|
724
768
|
|
|
@@ -873,4 +917,4 @@ export async function runTui () {
|
|
|
873
917
|
}
|
|
874
918
|
|
|
875
919
|
// Solo para pruebas headless (render sin terminal real). No usar en runtime.
|
|
876
|
-
export const __test = { render, profileRows, deviceRows, secretRows, pairModeRows, pairingBody, fitHelp, toggleLang }
|
|
920
|
+
export const __test = { render, profileRows, deviceRows, secretRows, pairModeRows, pairingBody, scrollBody, fitHelp, toggleLang }
|
package/src/tui/i18n.js
CHANGED
|
@@ -141,6 +141,7 @@ const es = {
|
|
|
141
141
|
pairWarning: '⚠ Este código deja LEER tus datos y FIRMAR con tu identidad. No lo compartas.',
|
|
142
142
|
pairConnected: (id) => `⧗ Se conectó: ${id} — pulsa A y escribe el código que muestra.`,
|
|
143
143
|
pairWaiting: 'Esperando a que el dispositivo se conecte…',
|
|
144
|
+
pairQrTooNarrow: (cols, need) => `Agranda el terminal (el QR necesita ${need} cols; hay ${cols}).`,
|
|
144
145
|
|
|
145
146
|
// confirmación / entrada
|
|
146
147
|
confirmKeys: ' (s / N)',
|
|
@@ -151,10 +152,10 @@ const es = {
|
|
|
151
152
|
// INGLÉS: new/rename/delete/password/unlock/locK/pair/approve/revoke/refresh/
|
|
152
153
|
// language/quit); lo único que se traduce es la palabra que las explica.
|
|
153
154
|
// Segmentos, no una línea: el render recorta del medio si no caben.
|
|
154
|
-
helpProfiles: ['↑↓', 'Enter entrar', 'n nueva', 'r renombrar', 'd borrar', '
|
|
155
|
+
helpProfiles: ['↑↓', 'Enter entrar', 'p emparejar', 'n nueva', 'r renombrar', 'd borrar', 'c clave', 'x quitar-clave', 'u desbloq', 'k bloquear', 'l English', 'q salir'],
|
|
155
156
|
helpDevices: ['←→ pestaña', '↑↓', 'p emparejar', 'a aprobar', 'x rechazar', 'v revocar', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
|
|
156
157
|
helpSecrets: ['←→ pestaña', '↑↓', 'n nueva variable', 'x quitar (variable/scope)', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
|
|
157
|
-
helpPairing: ['a aprobar', 'x rechazar', 'r reiniciar', 'Esc atrás', 'l English'],
|
|
158
|
+
helpPairing: ['a aprobar', 'x rechazar', 'r reiniciar', '↑↓ scroll', 'Esc atrás', 'l English'],
|
|
158
159
|
helpPairMode: ['↑↓', 'Enter elegir', 'Esc atrás', 'l English', 'q salir'],
|
|
159
160
|
|
|
160
161
|
// pantalla "daemon caído"
|
|
@@ -303,15 +304,16 @@ const en = {
|
|
|
303
304
|
pairWarning: '⚠ This code lets someone READ your data and SIGN as you. Do not share it.',
|
|
304
305
|
pairConnected: (id) => `⧗ Connected: ${id} — press A and type the code it shows.`,
|
|
305
306
|
pairWaiting: 'Waiting for the device to connect…',
|
|
307
|
+
pairQrTooNarrow: (cols, need) => `Widen the terminal (the QR needs ${need} cols; you have ${cols}).`,
|
|
306
308
|
|
|
307
309
|
confirmKeys: ' (y / N)',
|
|
308
310
|
helpInput: 'Enter confirm · Esc cancel · Ctrl-U clear',
|
|
309
311
|
helpConfirm: 'y confirm · n/Esc cancel',
|
|
310
312
|
|
|
311
|
-
helpProfiles: ['↑↓', 'Enter open', 'n new', 'r rename', 'd delete', '
|
|
313
|
+
helpProfiles: ['↑↓', 'Enter open', 'p pair', 'n new', 'r rename', 'd delete', 'c password', 'x drop-password', 'u unlock', 'k lock', 'l Español', 'q quit'],
|
|
312
314
|
helpDevices: ['←→ tab', '↑↓', 'p pair', 'a approve', 'x reject', 'v revoke', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
|
|
313
315
|
helpSecrets: ['←→ tab', '↑↓', 'n new variable', 'x remove (variable/scope)', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
|
|
314
|
-
helpPairing: ['a approve', 'x reject', 'r restart', 'Esc back', 'l Español'],
|
|
316
|
+
helpPairing: ['a approve', 'x reject', 'r restart', '↑↓ scroll', 'Esc back', 'l Español'],
|
|
315
317
|
helpPairMode: ['↑↓', 'Enter choose', 'Esc back', 'l Español', 'q quit'],
|
|
316
318
|
|
|
317
319
|
downTitle: 'The vault daemon is not running.',
|
package/src/vaultControl.js
CHANGED
|
@@ -262,7 +262,7 @@ export function pairUrl (qr) {
|
|
|
262
262
|
const payload = JSON.stringify(qr)
|
|
263
263
|
const b64 = Buffer.from(payload, 'utf8').toString('base64')
|
|
264
264
|
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
265
|
-
return { url: PROFILE_URL + b64, payload }
|
|
265
|
+
return { url: PROFILE_URL + b64, payload, b64 }
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
/**
|
|
@@ -279,11 +279,11 @@ export async function startPairing ({ profile, service } = {}) {
|
|
|
279
279
|
await sleep(100)
|
|
280
280
|
const pr = read(F.pair, null)
|
|
281
281
|
if (pr?.expiresAt > Date.now()) {
|
|
282
|
-
const { url, payload } = pairUrl(pr.qr)
|
|
282
|
+
const { url, payload, b64 } = pairUrl(pr.qr)
|
|
283
283
|
// `profile`/`profileName`: DE QUÉ CUENTA del vault sale este QR. El vault
|
|
284
284
|
// puede tener varias y el emparejamiento mete al dispositivo en UNA; la TUI
|
|
285
285
|
// y la CLI lo muestran para que no se enrole en la equivocada.
|
|
286
|
-
return { qr: pr.qr, expiresAt: pr.expiresAt, url, payload, profile: pr.profile || null, profileName: pr.profileName || '' }
|
|
286
|
+
return { qr: pr.qr, expiresAt: pr.expiresAt, url, payload, b64, profile: pr.profile || null, profileName: pr.profileName || '' }
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
289
|
throw coded('el daemon no inició el emparejamiento', 'PAIR_FAILED')
|