@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 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 (muestra el QR y la
107
- URL, y espera a que se conecte), **aprobar** con el código que muestra el
108
- dispositivo, **rechazar** y **revocar**.
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",
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
- case 'add': { const p = await mgr.add(req.name); return { done: `perfil creado: ${p.name || p.id}` } }
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
- const r = await guard(term, st, i.startingPairing, () => vc.startPairing({ profile: activeId(st) }))
386
- if (r.ok) { st.pairing = r.v; st.pending = null; st.screen = 'pairing' }
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) => (screen === 'profiles' ? i.titleProfiles : screen === 'pairing' ? i.titlePairing : '')
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',