@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.
@@ -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
+ }
@@ -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 new Error('el daemon no respondió')
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 new Error('el daemon no respondió')
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 new Error('el daemon no respondió')
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 new Error('el daemon no respondió')
221
- if (!(d.ns?.[ns] || []).includes(key)) throw new Error('el daemon no aplicó el cambio (revisa los logs del servicio)')
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 new Error('el daemon no respondió')
234
- if ((d.ns?.[ns] || []).includes(key)) throw new Error('el daemon no borró la variable (revisa los logs del servicio)')
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
- return { qr: pr.qr, expiresAt: pr.expiresAt, url, payload }
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 new Error('el daemon no inició el emparejamiento')
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. */