@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 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 creas una bóveda
104
- nueva, renombras, borras y pones/quitas/usas la contraseña (candado).
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` | password (Bóvedas) · pair emparejar (Dispositivos) | |
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.5",
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 ' + payload)
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. Por eso el candado dejó de
22
- * ser `l` (hoy idioma) y es `k`, la contraseña es `p` y emparejar es `p`.
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 === 'p' && cur) { // password
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 solo si entra cómodo (es "alto": ~ (módulos+8)/2 filas).
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
- const reserved = 9 // encabezado + cuenta + URL + payload + aviso
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.slice(0, contentH)
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', 'p clave', 'x quitar-clave', 'u desbloq', 'k bloquear', 'l English', 'q salir'],
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', 'p password', 'x drop-password', 'u unlock', 'k lock', 'l Español', 'q quit'],
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.',
@@ -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')