@dotrino/vaultd 0.7.2 → 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 +31 -3
- package/package.json +1 -1
- package/src/ctl.js +27 -5
- package/src/daemon.js +8 -2
- package/src/tui/app.js +292 -139
- package/src/tui/i18n.js +376 -0
- package/src/vaultControl.js +19 -9
package/src/tui/i18n.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n.js — textos de la TUI en español e inglés (CONVENCIONES §9: bilingüe es/en).
|
|
3
|
+
*
|
|
4
|
+
* Sin dependencias y sin estado global: `dict(lang)` devuelve el diccionario y la
|
|
5
|
+
* TUI guarda el idioma activo en `st.lang`. Las entradas con datos son funciones
|
|
6
|
+
* (`vaultCreated: (n) => …`) para que el orden de las palabras sea el natural de
|
|
7
|
+
* cada idioma en vez de una plantilla con huecos.
|
|
8
|
+
*
|
|
9
|
+
* Idioma inicial: `DOTRINO_LANG` (override por ejecución) → el guardado en
|
|
10
|
+
* `prefs.json` (la última vez que pulsaste `l`) → el locale del sistema
|
|
11
|
+
* (`LC_ALL`/`LC_MESSAGES`/`LANGUAGE`/`LANG`) → español.
|
|
12
|
+
*
|
|
13
|
+
* Español de Ecuador: TUTEO, nunca voseo (CONVENCIONES §9).
|
|
14
|
+
*/
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import { dataDir, readJson, writeJson } from '../paths.js'
|
|
17
|
+
|
|
18
|
+
// --------------------------------- español ---------------------------------
|
|
19
|
+
|
|
20
|
+
const es = {
|
|
21
|
+
code: 'es',
|
|
22
|
+
langName: 'Español',
|
|
23
|
+
otherLangName: 'English',
|
|
24
|
+
langChanged: 'Idioma: Español',
|
|
25
|
+
|
|
26
|
+
// encabezado / estado
|
|
27
|
+
daemonRunning: 'corriendo',
|
|
28
|
+
daemonStopped: 'DETENIDO',
|
|
29
|
+
activeVault: 'Bóveda activa: ',
|
|
30
|
+
noName: '(sin nombre)',
|
|
31
|
+
tooSmall: 'Terminal muy pequeño',
|
|
32
|
+
tooSmallHint: (cols, rows) => `Agranda a ≥ 24×9 (hay ${cols}×${rows}).`,
|
|
33
|
+
|
|
34
|
+
// pestañas y títulos
|
|
35
|
+
tabDevices: 'Dispositivos',
|
|
36
|
+
tabSecrets: 'Scopes y variables',
|
|
37
|
+
tabsHint: ' (←→ cambiar)',
|
|
38
|
+
titleProfiles: 'Bóvedas',
|
|
39
|
+
titlePairing: 'Emparejar un dispositivo',
|
|
40
|
+
titlePairMode: 'Emparejar: ¿a qué cuenta entra?',
|
|
41
|
+
|
|
42
|
+
// bóvedas (perfiles)
|
|
43
|
+
noPassword: 'sin clave',
|
|
44
|
+
locked: '🔒 bloqueada',
|
|
45
|
+
unlocked: '🔓 abierta',
|
|
46
|
+
passwordOf: (name) => `Contraseña de "${name}"`,
|
|
47
|
+
passwordToEdit: 'necesaria para editar la bóveda',
|
|
48
|
+
unlocking: 'Desbloqueando…',
|
|
49
|
+
loading: 'Cargando…',
|
|
50
|
+
loadingDevices: 'Cargando dispositivos…',
|
|
51
|
+
loadingSecrets: 'Cargando secretos…',
|
|
52
|
+
loadingVaults: 'Cargando bóvedas…',
|
|
53
|
+
switchingVault: 'Cambiando de bóveda…',
|
|
54
|
+
vaultNowActive: (name) => `Bóveda activa: ${name}`,
|
|
55
|
+
newVaultLabel: 'Nombre de la nueva bóveda',
|
|
56
|
+
newVaultHint: 'crea una identidad nueva y vacía',
|
|
57
|
+
nameEmpty: 'El nombre no puede estar vacío',
|
|
58
|
+
creatingVault: 'Creando bóveda…',
|
|
59
|
+
vaultCreated: (name) => `Bóveda creada: ${name}`,
|
|
60
|
+
renameLabel: (name) => `Nuevo nombre para "${name}"`,
|
|
61
|
+
renaming: 'Renombrando…',
|
|
62
|
+
vaultRenamed: 'Bóveda renombrada',
|
|
63
|
+
cantDeleteLast: 'No se puede borrar la única bóveda',
|
|
64
|
+
deleteLabel: (name) => `Escribe "${name}" para BORRARLA (irreversible)`,
|
|
65
|
+
deleteHint: 'se pierde su clave; sus dispositivos dejan de funcionar',
|
|
66
|
+
deleteMismatch: 'Cancelado (el nombre no coincide)',
|
|
67
|
+
deletingVault: 'Borrando bóveda…',
|
|
68
|
+
vaultDeleted: 'Bóveda borrada',
|
|
69
|
+
newPasswordLabel: (name) => `Contraseña nueva para "${name}" (mín. 4)`,
|
|
70
|
+
passwordTooShort: 'La contraseña debe tener al menos 4 caracteres',
|
|
71
|
+
repeatPassword: 'Repite la contraseña',
|
|
72
|
+
passwordMismatch: 'Las contraseñas no coinciden',
|
|
73
|
+
savingPassword: 'Guardando contraseña…',
|
|
74
|
+
passwordSaved: 'Contraseña guardada',
|
|
75
|
+
noPasswordSet: 'Esta bóveda no tiene contraseña',
|
|
76
|
+
removingPassword: 'Quitando contraseña…',
|
|
77
|
+
passwordRemoved: 'Contraseña quitada',
|
|
78
|
+
alreadyUnlocked: 'Ya está desbloqueada',
|
|
79
|
+
vaultUnlocked: 'Bóveda desbloqueada',
|
|
80
|
+
lockingVault: 'Bloqueando…',
|
|
81
|
+
vaultLocked: 'Bóveda bloqueada',
|
|
82
|
+
|
|
83
|
+
// dispositivos
|
|
84
|
+
pendingDevice: (id) => ` ⧗ PENDIENTE: ${id}`,
|
|
85
|
+
pendingHint: ' — pulsa A para aprobar, X para rechazar',
|
|
86
|
+
noDevices: ' (sin dispositivos enrolados — pulsa P para emparejar uno)',
|
|
87
|
+
noLabel: '(sin etiqueta)',
|
|
88
|
+
revokedCount: (n) => ` Revocados: ${n}`,
|
|
89
|
+
startingPairing: 'Iniciando emparejamiento…',
|
|
90
|
+
noPending: 'No hay ningún dispositivo pendiente',
|
|
91
|
+
noPendingToReject: 'No hay ningún dispositivo pendiente para rechazar',
|
|
92
|
+
rejecting: 'Rechazando…',
|
|
93
|
+
deviceRejected: 'Dispositivo rechazado',
|
|
94
|
+
revokeConfirm: (id) => `¿Revocar ${id}? Se le ordena autoborrarse al reconectar.`,
|
|
95
|
+
revoking: 'Revocando…',
|
|
96
|
+
deviceRevoked: (id) => `Revocado ${id}`,
|
|
97
|
+
approveLabel: (id) => `Código que MUESTRA el dispositivo ${id}`,
|
|
98
|
+
approveHint: 'el vault no lo conoce: compáralo en la otra pantalla',
|
|
99
|
+
codeMissing: 'Falta el código',
|
|
100
|
+
approving: 'Aprobando…',
|
|
101
|
+
deviceApproved: 'Dispositivo aprobado',
|
|
102
|
+
restartingPairing: 'Reiniciando emparejamiento…',
|
|
103
|
+
|
|
104
|
+
// scopes y variables
|
|
105
|
+
noScopes: ' (sin scopes — pulsa N para agregar la primera variable)',
|
|
106
|
+
scopeOf: (ns) => ` (scope vault:secrets:${ns})`,
|
|
107
|
+
removeVarConfirm: (ns, key) => `¿Quitar la variable ${ns}/${key}?`,
|
|
108
|
+
removingVar: 'Quitando variable…',
|
|
109
|
+
varRemoved: 'Variable quitada',
|
|
110
|
+
removeScopeConfirm: (ns, n) => `¿Quitar el scope "${ns}" ENTERO (${n} variable(s))?`,
|
|
111
|
+
removingScope: 'Quitando scope…',
|
|
112
|
+
scopeRemoved: (ns) => `Scope "${ns}" quitado`,
|
|
113
|
+
nsLabel: 'Scope (namespace del servicio)',
|
|
114
|
+
nsHintExisting: (list) => `[a-z0-9-] · existen: ${list}`,
|
|
115
|
+
nsHint: '[a-z0-9-], p. ej. proxy',
|
|
116
|
+
nsInvalid: 'Scope inválido: usa [a-z0-9-]{1,32}',
|
|
117
|
+
keyLabel: (ns) => `Variable en "${ns}" (MAYUSCULAS_CON_GUION_BAJO)`,
|
|
118
|
+
keyHint: '[A-Z0-9_], p. ej. TURN_KEY_ID',
|
|
119
|
+
keyInvalid: 'Clave inválida: usa [A-Z0-9_]{1,64}',
|
|
120
|
+
valueLabel: (ns, key) => `Valor de ${ns}/${key}`,
|
|
121
|
+
valueHint: 'el valor nunca se muestra; se guarda en la bóveda',
|
|
122
|
+
valueEmpty: 'El valor no puede estar vacío',
|
|
123
|
+
savingVar: 'Guardando variable…',
|
|
124
|
+
varSaved: (ns, key) => `Guardado ${ns}/${key}`,
|
|
125
|
+
|
|
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}`,
|
|
137
|
+
pairAccount: (name) => `Cuenta que se comparte: ${name}`,
|
|
138
|
+
pairValid: (min) => `Válido ~${min} min. Escanéalo o abre la URL en el dispositivo.`,
|
|
139
|
+
pairUrl: 'URL: ',
|
|
140
|
+
pairPaste: 'O pega este código en la pestaña #vault de profile.dotrino.com:',
|
|
141
|
+
pairWarning: '⚠ Este código deja LEER tus datos y FIRMAR con tu identidad. No lo compartas.',
|
|
142
|
+
pairConnected: (id) => `⧗ Se conectó: ${id} — pulsa A y escribe el código que muestra.`,
|
|
143
|
+
pairWaiting: 'Esperando a que el dispositivo se conecte…',
|
|
144
|
+
|
|
145
|
+
// confirmación / entrada
|
|
146
|
+
confirmKeys: ' (s / N)',
|
|
147
|
+
helpInput: 'Enter confirmar · Esc cancelar · Ctrl-U limpiar',
|
|
148
|
+
helpConfirm: 's confirmar · n/Esc cancelar',
|
|
149
|
+
|
|
150
|
+
// Barras de ayuda. Las TECLAS son las mismas en los dos idiomas (mnemónico
|
|
151
|
+
// INGLÉS: new/rename/delete/password/unlock/locK/pair/approve/revoke/refresh/
|
|
152
|
+
// language/quit); lo único que se traduce es la palabra que las explica.
|
|
153
|
+
// 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
|
+
helpDevices: ['←→ pestaña', '↑↓', 'p emparejar', 'a aprobar', 'x rechazar', 'v revocar', 'r refrescar', 'Esc bóvedas', 'l English', 'q salir'],
|
|
156
|
+
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
|
+
helpPairMode: ['↑↓', 'Enter elegir', 'Esc atrás', 'l English', 'q salir'],
|
|
159
|
+
|
|
160
|
+
// pantalla "daemon caído"
|
|
161
|
+
downTitle: 'El daemon del vault no está corriendo.',
|
|
162
|
+
downBody1: 'La TUI le da órdenes al daemon (custodio de tu clave). Sin él no puede',
|
|
163
|
+
downBody2: 'crear bóvedas, listar dispositivos ni tocar secretos.',
|
|
164
|
+
downStart: ' intentar arrancarlo: ',
|
|
165
|
+
downRecheck: ' volver a comprobar',
|
|
166
|
+
downLang: ' cambiar a English',
|
|
167
|
+
downQuit: ' salir',
|
|
168
|
+
downDev: 'En desarrollo, arráncalo a mano: node bin/dotrino-vaultd.js',
|
|
169
|
+
downHeader: 'dotrino-vault daemon: DETENIDO',
|
|
170
|
+
downHelp: ['S arrancar', 'R comprobar', 'l English', 'Q salir'],
|
|
171
|
+
starting: 'Arrancando el servicio…',
|
|
172
|
+
startingShort: 'Arrancando…',
|
|
173
|
+
stillDown: 'Sigue sin responder',
|
|
174
|
+
startedNotReady: 'Arrancó pero aún no responde; pulsa R',
|
|
175
|
+
startFailed: (err) => `No se pudo arrancar: ${err}`,
|
|
176
|
+
|
|
177
|
+
// errores
|
|
178
|
+
errDaemonDown: 'El daemon no está corriendo. Arráncalo: systemctl --user start dotrino-vault (o reinicia la TUI).',
|
|
179
|
+
errNoReply: 'El daemon no respondió.',
|
|
180
|
+
errNotApplied: 'El daemon no aplicó el cambio (revisa los logs del servicio).',
|
|
181
|
+
errNotDeleted: 'El daemon no borró la variable (revisa los logs del servicio).',
|
|
182
|
+
errPairFailed: 'El daemon no inició el emparejamiento.'
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------- inglés ----------------------------------
|
|
186
|
+
|
|
187
|
+
const en = {
|
|
188
|
+
code: 'en',
|
|
189
|
+
langName: 'English',
|
|
190
|
+
otherLangName: 'Español',
|
|
191
|
+
langChanged: 'Language: English',
|
|
192
|
+
|
|
193
|
+
daemonRunning: 'running',
|
|
194
|
+
daemonStopped: 'STOPPED',
|
|
195
|
+
activeVault: 'Active vault: ',
|
|
196
|
+
noName: '(unnamed)',
|
|
197
|
+
tooSmall: 'Terminal too small',
|
|
198
|
+
tooSmallHint: (cols, rows) => `Resize to ≥ 24×9 (now ${cols}×${rows}).`,
|
|
199
|
+
|
|
200
|
+
tabDevices: 'Devices',
|
|
201
|
+
tabSecrets: 'Scopes & variables',
|
|
202
|
+
tabsHint: ' (←→ switch)',
|
|
203
|
+
titleProfiles: 'Vaults',
|
|
204
|
+
titlePairing: 'Pair a device',
|
|
205
|
+
titlePairMode: 'Pairing: which account does it join?',
|
|
206
|
+
|
|
207
|
+
noPassword: 'no password',
|
|
208
|
+
locked: '🔒 locked',
|
|
209
|
+
unlocked: '🔓 unlocked',
|
|
210
|
+
passwordOf: (name) => `Password for "${name}"`,
|
|
211
|
+
passwordToEdit: 'needed to edit this vault',
|
|
212
|
+
unlocking: 'Unlocking…',
|
|
213
|
+
loading: 'Loading…',
|
|
214
|
+
loadingDevices: 'Loading devices…',
|
|
215
|
+
loadingSecrets: 'Loading secrets…',
|
|
216
|
+
loadingVaults: 'Loading vaults…',
|
|
217
|
+
switchingVault: 'Switching vault…',
|
|
218
|
+
vaultNowActive: (name) => `Active vault: ${name}`,
|
|
219
|
+
newVaultLabel: 'Name of the new vault',
|
|
220
|
+
newVaultHint: 'creates a new, empty identity',
|
|
221
|
+
nameEmpty: 'The name cannot be empty',
|
|
222
|
+
creatingVault: 'Creating vault…',
|
|
223
|
+
vaultCreated: (name) => `Vault created: ${name}`,
|
|
224
|
+
renameLabel: (name) => `New name for "${name}"`,
|
|
225
|
+
renaming: 'Renaming…',
|
|
226
|
+
vaultRenamed: 'Vault renamed',
|
|
227
|
+
cantDeleteLast: 'Cannot delete the only vault',
|
|
228
|
+
deleteLabel: (name) => `Type "${name}" to DELETE it (irreversible)`,
|
|
229
|
+
deleteHint: 'its key is lost; its devices stop working',
|
|
230
|
+
deleteMismatch: 'Cancelled (the name does not match)',
|
|
231
|
+
deletingVault: 'Deleting vault…',
|
|
232
|
+
vaultDeleted: 'Vault deleted',
|
|
233
|
+
newPasswordLabel: (name) => `New password for "${name}" (min. 4)`,
|
|
234
|
+
passwordTooShort: 'The password must be at least 4 characters',
|
|
235
|
+
repeatPassword: 'Repeat the password',
|
|
236
|
+
passwordMismatch: 'The passwords do not match',
|
|
237
|
+
savingPassword: 'Saving password…',
|
|
238
|
+
passwordSaved: 'Password saved',
|
|
239
|
+
noPasswordSet: 'This vault has no password',
|
|
240
|
+
removingPassword: 'Removing password…',
|
|
241
|
+
passwordRemoved: 'Password removed',
|
|
242
|
+
alreadyUnlocked: 'Already unlocked',
|
|
243
|
+
vaultUnlocked: 'Vault unlocked',
|
|
244
|
+
lockingVault: 'Locking…',
|
|
245
|
+
vaultLocked: 'Vault locked',
|
|
246
|
+
|
|
247
|
+
pendingDevice: (id) => ` ⧗ PENDING: ${id}`,
|
|
248
|
+
pendingHint: ' — press A to approve, X to reject',
|
|
249
|
+
noDevices: ' (no devices enrolled — press P to pair one)',
|
|
250
|
+
noLabel: '(no label)',
|
|
251
|
+
revokedCount: (n) => ` Revoked: ${n}`,
|
|
252
|
+
startingPairing: 'Starting pairing…',
|
|
253
|
+
noPending: 'No device is waiting',
|
|
254
|
+
noPendingToReject: 'No device is waiting to be rejected',
|
|
255
|
+
rejecting: 'Rejecting…',
|
|
256
|
+
deviceRejected: 'Device rejected',
|
|
257
|
+
revokeConfirm: (id) => `Revoke ${id}? It is told to erase itself on reconnect.`,
|
|
258
|
+
revoking: 'Revoking…',
|
|
259
|
+
deviceRevoked: (id) => `Revoked ${id}`,
|
|
260
|
+
approveLabel: (id) => `Code SHOWN by device ${id}`,
|
|
261
|
+
approveHint: 'the vault does not know it: compare it on the other screen',
|
|
262
|
+
codeMissing: 'The code is missing',
|
|
263
|
+
approving: 'Approving…',
|
|
264
|
+
deviceApproved: 'Device approved',
|
|
265
|
+
restartingPairing: 'Restarting pairing…',
|
|
266
|
+
|
|
267
|
+
noScopes: ' (no scopes — press N to add the first variable)',
|
|
268
|
+
scopeOf: (ns) => ` (scope vault:secrets:${ns})`,
|
|
269
|
+
removeVarConfirm: (ns, key) => `Remove the variable ${ns}/${key}?`,
|
|
270
|
+
removingVar: 'Removing variable…',
|
|
271
|
+
varRemoved: 'Variable removed',
|
|
272
|
+
removeScopeConfirm: (ns, n) => `Remove the WHOLE scope "${ns}" (${n} variable(s))?`,
|
|
273
|
+
removingScope: 'Removing scope…',
|
|
274
|
+
scopeRemoved: (ns) => `Scope "${ns}" removed`,
|
|
275
|
+
nsLabel: 'Scope (the service namespace)',
|
|
276
|
+
nsHintExisting: (list) => `[a-z0-9-] · existing: ${list}`,
|
|
277
|
+
nsHint: '[a-z0-9-], e.g. proxy',
|
|
278
|
+
nsInvalid: 'Invalid scope: use [a-z0-9-]{1,32}',
|
|
279
|
+
keyLabel: (ns) => `Variable in "${ns}" (UPPERCASE_WITH_UNDERSCORES)`,
|
|
280
|
+
keyHint: '[A-Z0-9_], e.g. TURN_KEY_ID',
|
|
281
|
+
keyInvalid: 'Invalid key: use [A-Z0-9_]{1,64}',
|
|
282
|
+
valueLabel: (ns, key) => `Value of ${ns}/${key}`,
|
|
283
|
+
valueHint: 'the value is never shown; it is kept in the vault',
|
|
284
|
+
valueEmpty: 'The value cannot be empty',
|
|
285
|
+
savingVar: 'Saving variable…',
|
|
286
|
+
varSaved: (ns, key) => `Saved ${ns}/${key}`,
|
|
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}`,
|
|
298
|
+
pairAccount: (name) => `Account being shared: ${name}`,
|
|
299
|
+
pairValid: (min) => `Valid ~${min} min. Scan it or open the URL on the device.`,
|
|
300
|
+
pairUrl: 'URL: ',
|
|
301
|
+
pairPaste: 'Or paste this code into the #vault tab of profile.dotrino.com:',
|
|
302
|
+
pairWarning: '⚠ This code lets someone READ your data and SIGN as you. Do not share it.',
|
|
303
|
+
pairConnected: (id) => `⧗ Connected: ${id} — press A and type the code it shows.`,
|
|
304
|
+
pairWaiting: 'Waiting for the device to connect…',
|
|
305
|
+
|
|
306
|
+
confirmKeys: ' (y / N)',
|
|
307
|
+
helpInput: 'Enter confirm · Esc cancel · Ctrl-U clear',
|
|
308
|
+
helpConfirm: 'y confirm · n/Esc cancel',
|
|
309
|
+
|
|
310
|
+
helpProfiles: ['↑↓', 'Enter open', 'n new', 'r rename', 'd delete', 'p password', 'x drop-password', 'u unlock', 'k lock', 'l Español', 'q quit'],
|
|
311
|
+
helpDevices: ['←→ tab', '↑↓', 'p pair', 'a approve', 'x reject', 'v revoke', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
|
|
312
|
+
helpSecrets: ['←→ tab', '↑↓', 'n new variable', 'x remove (variable/scope)', 'r refresh', 'Esc vaults', 'l Español', 'q quit'],
|
|
313
|
+
helpPairing: ['a approve', 'x reject', 'r restart', 'Esc back', 'l Español'],
|
|
314
|
+
helpPairMode: ['↑↓', 'Enter choose', 'Esc back', 'l Español', 'q quit'],
|
|
315
|
+
|
|
316
|
+
downTitle: 'The vault daemon is not running.',
|
|
317
|
+
downBody1: 'The TUI gives orders to the daemon (the keeper of your key). Without it',
|
|
318
|
+
downBody2: 'it cannot create vaults, list devices or touch secrets.',
|
|
319
|
+
downStart: ' try to start it: ',
|
|
320
|
+
downRecheck: ' check again',
|
|
321
|
+
downLang: ' switch to Español',
|
|
322
|
+
downQuit: ' quit',
|
|
323
|
+
downDev: 'In development, start it by hand: node bin/dotrino-vaultd.js',
|
|
324
|
+
downHeader: 'dotrino-vault daemon: STOPPED',
|
|
325
|
+
downHelp: ['S start', 'R check', 'l Español', 'Q quit'],
|
|
326
|
+
starting: 'Starting the service…',
|
|
327
|
+
startingShort: 'Starting…',
|
|
328
|
+
stillDown: 'Still not answering',
|
|
329
|
+
startedNotReady: 'It started but does not answer yet; press R',
|
|
330
|
+
startFailed: (err) => `Could not start it: ${err}`,
|
|
331
|
+
|
|
332
|
+
errDaemonDown: 'The daemon is not running. Start it: systemctl --user start dotrino-vault (or restart the TUI).',
|
|
333
|
+
errNoReply: 'The daemon did not answer.',
|
|
334
|
+
errNotApplied: 'The daemon did not apply the change (check the service logs).',
|
|
335
|
+
errNotDeleted: 'The daemon did not delete the variable (check the service logs).',
|
|
336
|
+
errPairFailed: 'The daemon did not start the pairing.'
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// --------------------------- selección y persistencia -----------------------
|
|
340
|
+
|
|
341
|
+
export const LANGS = ['es', 'en']
|
|
342
|
+
|
|
343
|
+
/** Diccionario del idioma pedido (español para cualquier valor desconocido). */
|
|
344
|
+
export const dict = (lang) => (lang === 'en' ? en : es)
|
|
345
|
+
|
|
346
|
+
/** El OTRO idioma (el toggle es binario). */
|
|
347
|
+
export const otherLang = (lang) => (lang === 'en' ? 'es' : 'en')
|
|
348
|
+
|
|
349
|
+
const prefsFile = () => path.join(dataDir(), 'prefs.json')
|
|
350
|
+
|
|
351
|
+
/** 'es_EC.UTF-8' → 'es'; 'C'/'POSIX'/vacío → null (para caer al siguiente origen). */
|
|
352
|
+
const normalize = (v) => {
|
|
353
|
+
const s = String(v || '').toLowerCase()
|
|
354
|
+
if (s.startsWith('en')) return 'en'
|
|
355
|
+
if (s.startsWith('es')) return 'es'
|
|
356
|
+
return null
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Idioma inicial: DOTRINO_LANG → prefs.json → locale del sistema → español. */
|
|
360
|
+
export function loadLang () {
|
|
361
|
+
const forced = normalize(process.env.DOTRINO_LANG)
|
|
362
|
+
if (forced) return forced
|
|
363
|
+
const saved = normalize(readJson(prefsFile(), {})?.lang)
|
|
364
|
+
if (saved) return saved
|
|
365
|
+
const locale = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANGUAGE || process.env.LANG
|
|
366
|
+
return normalize(locale) || 'es'
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Recuerda el idioma para la próxima vez (junto al resto de preferencias). */
|
|
370
|
+
export function saveLang (lang) {
|
|
371
|
+
if (!LANGS.includes(lang)) return false
|
|
372
|
+
try {
|
|
373
|
+
writeJson(prefsFile(), { ...(readJson(prefsFile(), {}) || {}), lang })
|
|
374
|
+
return true
|
|
375
|
+
} catch (_) { return false } // preferencia: nunca romper la TUI por no poder guardarla
|
|
376
|
+
}
|
package/src/vaultControl.js
CHANGED
|
@@ -67,6 +67,13 @@ class DaemonDownError extends Error {
|
|
|
67
67
|
constructor () { super('el daemon del vault no está corriendo'); this.code = 'DAEMON_DOWN' }
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Error con `code`: la CLI sigue imprimiendo el mensaje tal cual y la TUI, que es
|
|
72
|
+
* bilingüe (`src/tui/i18n.js`), lo traduce por el código. Los errores que REENVÍA
|
|
73
|
+
* el daemon no llevan código: son diagnósticos del servicio, no copy de interfaz.
|
|
74
|
+
*/
|
|
75
|
+
const coded = (message, code) => Object.assign(new Error(message), { code })
|
|
76
|
+
|
|
70
77
|
/**
|
|
71
78
|
* Exige el daemon vivo ANTES de escribir cualquier petición. Es clave para las
|
|
72
79
|
* peticiones que llevan secretos (contraseña de perfil, valor de secreto): si el
|
|
@@ -141,7 +148,7 @@ async function profileOp (op, { profile, name, password } = {}) {
|
|
|
141
148
|
writeReq(F.profileReq, { op, ...extra }, profile)
|
|
142
149
|
signalOrCleanup('SIGUSR2', [F.profileReq])
|
|
143
150
|
const d = await waitFor(F.profilesList)
|
|
144
|
-
if (!d) throw
|
|
151
|
+
if (!d) throw coded('el daemon no respondió', 'NO_REPLY')
|
|
145
152
|
if (d.error) throw new Error(d.error)
|
|
146
153
|
return d // { profiles:[{id,name,protected,locked,current,fingerprint,iss,createdAt}], current, done? }
|
|
147
154
|
}
|
|
@@ -181,7 +188,7 @@ export async function snapshot (profile) {
|
|
|
181
188
|
*/
|
|
182
189
|
export async function listDevices (profile) {
|
|
183
190
|
const { devices } = await snapshot(profile)
|
|
184
|
-
if (!devices) throw
|
|
191
|
+
if (!devices) throw coded('el daemon no respondió', 'NO_REPLY')
|
|
185
192
|
const issued = devices.issued || devices.active || devices.delegations || []
|
|
186
193
|
const withIds = await Promise.all(issued.map(async (d) => ({
|
|
187
194
|
...d, deviceId: d.sub ? await deviceIdOf(d.sub) : '????-????'
|
|
@@ -205,7 +212,7 @@ export async function revokeDevice (nonce, profile) {
|
|
|
205
212
|
/** Scopes→[claves] del perfil (NUNCA los valores; el daemon no los expone). */
|
|
206
213
|
export async function listSecrets (profile) {
|
|
207
214
|
const { secrets } = await snapshot(profile)
|
|
208
|
-
if (!secrets) throw
|
|
215
|
+
if (!secrets) throw coded('el daemon no respondió', 'NO_REPLY')
|
|
209
216
|
return secrets.ns || {}
|
|
210
217
|
}
|
|
211
218
|
|
|
@@ -217,8 +224,8 @@ export async function setSecret (ns, key, value, profile) {
|
|
|
217
224
|
writeReq(F.dumpReq, {}, profile)
|
|
218
225
|
signalOrCleanup('SIGUSR2', [F.secretReq, F.dumpReq])
|
|
219
226
|
const d = await waitFor(F.secretsList)
|
|
220
|
-
if (!d) throw
|
|
221
|
-
if (!(d.ns?.[ns] || []).includes(key)) throw
|
|
227
|
+
if (!d) throw coded('el daemon no respondió', 'NO_REPLY')
|
|
228
|
+
if (!(d.ns?.[ns] || []).includes(key)) throw coded('el daemon no aplicó el cambio (revisa los logs del servicio)', 'NOT_APPLIED')
|
|
222
229
|
return d.ns
|
|
223
230
|
}
|
|
224
231
|
|
|
@@ -230,8 +237,8 @@ export async function deleteSecret (ns, key, profile) {
|
|
|
230
237
|
writeReq(F.dumpReq, {}, profile)
|
|
231
238
|
signalOrCleanup('SIGUSR2', [F.secretReq, F.dumpReq])
|
|
232
239
|
const d = await waitFor(F.secretsList)
|
|
233
|
-
if (!d) throw
|
|
234
|
-
if ((d.ns?.[ns] || []).includes(key)) throw
|
|
240
|
+
if (!d) throw coded('el daemon no respondió', 'NO_REPLY')
|
|
241
|
+
if ((d.ns?.[ns] || []).includes(key)) throw coded('el daemon no borró la variable (revisa los logs del servicio)', 'NOT_DELETED')
|
|
235
242
|
return d.ns
|
|
236
243
|
}
|
|
237
244
|
|
|
@@ -273,10 +280,13 @@ export async function startPairing ({ profile, service } = {}) {
|
|
|
273
280
|
const pr = read(F.pair, null)
|
|
274
281
|
if (pr?.expiresAt > Date.now()) {
|
|
275
282
|
const { url, payload } = pairUrl(pr.qr)
|
|
276
|
-
|
|
283
|
+
// `profile`/`profileName`: DE QUÉ CUENTA del vault sale este QR. El vault
|
|
284
|
+
// puede tener varias y el emparejamiento mete al dispositivo en UNA; la TUI
|
|
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 || '' }
|
|
277
287
|
}
|
|
278
288
|
}
|
|
279
|
-
throw
|
|
289
|
+
throw coded('el daemon no inició el emparejamiento', 'PAIR_FAILED')
|
|
280
290
|
}
|
|
281
291
|
|
|
282
292
|
/** Dispositivo pendiente de aprobar (el que se conectó con el QR), o null. */
|