@dotrino/vaultd 0.26.2 → 0.38.0
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 +142 -25
- package/bin/dotrino-vaultd.js +4 -4
- package/lib/README.md +13 -2
- package/lib/src/admin.js +88 -3
- package/lib/src/config.js +1 -1
- package/lib/src/enroll.js +26 -22
- package/lib/src/env.js +37 -17
- package/lib/src/envtext.js +94 -0
- package/lib/src/index.js +4 -4
- package/lib/src/invite.js +8 -8
- package/lib/src/protocol.js +12 -0
- package/lib/src/service.js +218 -92
- package/package.json +9 -4
- package/src/ctl.js +375 -90
- package/src/daemon.js +167 -45
- package/src/manager.js +6 -6
- package/src/profiles.js +27 -4
- package/src/secretsStore.js +191 -25
- package/src/transport.js +2 -2
- package/src/tui/app.js +512 -126
- package/src/tui/i18n.js +118 -22
- package/src/vault.js +351 -44
- package/src/vaultControl.js +253 -64
package/src/ctl.js
CHANGED
|
@@ -25,6 +25,8 @@ import path from 'node:path'
|
|
|
25
25
|
import { execFileSync } from 'node:child_process'
|
|
26
26
|
import { pubkeyId } from '@dotrino/identity/capabilities'
|
|
27
27
|
import { dataDir, readJson } from './paths.js'
|
|
28
|
+
import { assertVar } from './secretsStore.js'
|
|
29
|
+
import { parseEnvText, PAIR_RE } from '../lib/src/envtext.js'
|
|
28
30
|
import { qrToString } from './qr.js'
|
|
29
31
|
import { encodeInvite, inviteUrl } from '../lib/src/invite.js'
|
|
30
32
|
import { VERSION } from './version.js'
|
|
@@ -49,7 +51,18 @@ function takeProfileFlag (args) {
|
|
|
49
51
|
}
|
|
50
52
|
/** Campo `profile` de las peticiones al daemon (omitido = perfil activo). */
|
|
51
53
|
const withProfile = (obj) => (PROFILE ? { ...obj, profile: PROFILE } : obj)
|
|
52
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Deja una petición para el daemon. ATÓMICA (escribir aparte y renombrar): el daemon
|
|
56
|
+
* vigila la carpeta con `fs.watch`, que avisa al CREAR el archivo y no al terminar de
|
|
57
|
+
* escribirlo, así que escrito en el sitio se puede leer a medias — y una petición que no
|
|
58
|
+
* parsea se pierde con todos sus datos.
|
|
59
|
+
*/
|
|
60
|
+
const writeReq = (name, obj) => {
|
|
61
|
+
const dest = path.join(dir, name)
|
|
62
|
+
const tmp = dest + '.tmp'
|
|
63
|
+
fs.writeFileSync(tmp, JSON.stringify(withProfile({ ...obj, at: Date.now() })), { mode: 0o600 })
|
|
64
|
+
fs.renameSync(tmp, dest)
|
|
65
|
+
}
|
|
53
66
|
|
|
54
67
|
const R = '\x1b[31m', B = '\x1b[1m', Z = '\x1b[0m' // rojo / negrita / reset
|
|
55
68
|
// La versión se inyecta en build (esbuild --define); en dev cae a 'dev'.
|
|
@@ -60,13 +73,13 @@ const R = '\x1b[31m', B = '\x1b[1m', Z = '\x1b[0m' // rojo / negrita / reset
|
|
|
60
73
|
* contenedor, así que decir siempre «systemctl» manda a la mitad de la gente a un
|
|
61
74
|
* comando que no existe.
|
|
62
75
|
*/
|
|
63
|
-
const
|
|
64
|
-
const START_HINT =
|
|
76
|
+
const IN_DOCKER = !!process.env.DOTRINO_IN_DOCKER
|
|
77
|
+
const START_HINT = IN_DOCKER
|
|
65
78
|
? 'docker start dotrino-vault'
|
|
66
79
|
: process.platform === 'linux'
|
|
67
80
|
? 'systemctl --user start dotrino-vault'
|
|
68
81
|
: 'dotrino-vaultd (o: npx -y @dotrino/vaultd)'
|
|
69
|
-
const RESTART_HINT =
|
|
82
|
+
const RESTART_HINT = IN_DOCKER
|
|
70
83
|
? 'docker restart dotrino-vault'
|
|
71
84
|
: process.platform === 'linux'
|
|
72
85
|
? 'systemctl --user restart dotrino-vault'
|
|
@@ -88,7 +101,7 @@ function alive (pid) { try { return !!pid && (process.kill(pid, 0) || true) } ca
|
|
|
88
101
|
* vigila la carpeta igualmente, así que la petición se atiende en cuanto se escribe el
|
|
89
102
|
* archivo; la señal solo se ahorra la latencia del watcher.
|
|
90
103
|
*/
|
|
91
|
-
function
|
|
104
|
+
function sendSignal (pid, sig) { try { process.kill(pid, sig) } catch (_) { /* Windows, o el daemon ya se enteró */ } }
|
|
92
105
|
function sleep (ms) { return new Promise((r) => setTimeout(r, ms)) }
|
|
93
106
|
function requireDaemon () {
|
|
94
107
|
const s = readState()
|
|
@@ -138,6 +151,20 @@ function showChallenge (pe) {
|
|
|
138
151
|
console.log(' Si no reconoces este dispositivo: dotrino-vault reject %s\n', pe.deviceId)
|
|
139
152
|
}
|
|
140
153
|
|
|
154
|
+
/**
|
|
155
|
+
* EL CANDADO. Un perfil con contraseña y bloqueado no se ve ni se toca desde esta consola:
|
|
156
|
+
* el daemon contesta `locked` y sin contenido, y aquí se corta con un mensaje que dice qué
|
|
157
|
+
* hacer. Los aparatos ya emparejados siguen atendidos — lo que está cerrado es esta
|
|
158
|
+
* pantalla, no la bóveda.
|
|
159
|
+
*/
|
|
160
|
+
function assertOpen (d) {
|
|
161
|
+
if (d?.locked) {
|
|
162
|
+
console.error('Perfil bloqueado. Ábrelo con: dotrino-vault unlock')
|
|
163
|
+
process.exit(1)
|
|
164
|
+
}
|
|
165
|
+
return d
|
|
166
|
+
}
|
|
167
|
+
|
|
141
168
|
async function cmdPair (args = []) {
|
|
142
169
|
const s = requireDaemon()
|
|
143
170
|
try { fs.rmSync(pairFile, { force: true }) } catch (_) {}
|
|
@@ -184,10 +211,15 @@ async function cmdPair (args = []) {
|
|
|
184
211
|
// La petición se escribe SIEMPRE (aunque no haya --service): lleva a qué perfil
|
|
185
212
|
// se empareja el dispositivo.
|
|
186
213
|
writeReq('pair-request.json', { ...(service ? { service } : {}), ...(adopt ? { mode: 'adopt' } : {}) })
|
|
187
|
-
|
|
214
|
+
sendSignal(s.pid, 'SIGUSR1')
|
|
188
215
|
|
|
189
216
|
let pair = null
|
|
190
|
-
for (let i = 0; i < 50; i++) {
|
|
217
|
+
for (let i = 0; i < 50; i++) {
|
|
218
|
+
await sleep(100)
|
|
219
|
+
const p = readJson(pairFile, null)
|
|
220
|
+
assertOpen(p) // el candado se contesta por el mismo archivo, para no dejar esperando
|
|
221
|
+
if (p?.expiresAt > Date.now()) { pair = p; break }
|
|
222
|
+
}
|
|
191
223
|
if (!pair) { console.error('No se recibió respuesta del daemon para el emparejamiento.'); process.exit(1) }
|
|
192
224
|
|
|
193
225
|
// Una sola forma para las dos cosas: la invitación compacta (base64url de ~100
|
|
@@ -237,7 +269,7 @@ function cmdApprove (code) {
|
|
|
237
269
|
if (!code) { console.error('uso: dotrino-vault approve <código> (los dígitos que muestra el dispositivo)'); process.exit(2) }
|
|
238
270
|
const s = requireDaemon()
|
|
239
271
|
writeReq('approve-request.json', { code: String(code) })
|
|
240
|
-
|
|
272
|
+
sendSignal(s.pid, 'SIGUSR2')
|
|
241
273
|
console.log('Aprobando con el código %s… verifica con: dotrino-vault devices', code)
|
|
242
274
|
}
|
|
243
275
|
|
|
@@ -245,7 +277,7 @@ function cmdReject (deviceId) {
|
|
|
245
277
|
if (!deviceId) { console.error('uso: dotrino-vault reject <deviceId>'); process.exit(2) }
|
|
246
278
|
const s = requireDaemon()
|
|
247
279
|
writeReq('reject-request.json', { deviceId })
|
|
248
|
-
|
|
280
|
+
sendSignal(s.pid, 'SIGUSR2')
|
|
249
281
|
console.log('Rechazado %s.', deviceId)
|
|
250
282
|
}
|
|
251
283
|
|
|
@@ -265,12 +297,13 @@ async function cmdMe () {
|
|
|
265
297
|
const meFile = path.join(dir, 'me.json')
|
|
266
298
|
try { fs.rmSync(meFile, { force: true }) } catch (_) {}
|
|
267
299
|
writeReq('me-request.json', {})
|
|
268
|
-
|
|
300
|
+
sendSignal(s.pid, 'SIGUSR2')
|
|
269
301
|
let dump = null
|
|
270
302
|
for (let n = 0; n < 50; n++) { await sleep(100); const d = readJson(meFile, null); if (d?.at) { dump = d; break } }
|
|
271
303
|
// El volcado es contenido del usuario: se lee y se BORRA, no se queda ahí suelto.
|
|
272
304
|
try { fs.rmSync(meFile, { force: true }) } catch (_) {}
|
|
273
305
|
if (!dump) { console.error('La bóveda no respondió. ¿Está corriendo? dotrino-vault status'); process.exit(1) }
|
|
306
|
+
assertOpen(dump)
|
|
274
307
|
|
|
275
308
|
const me = dump.me
|
|
276
309
|
if (!me) {
|
|
@@ -279,8 +312,8 @@ async function cmdMe () {
|
|
|
279
312
|
return
|
|
280
313
|
}
|
|
281
314
|
|
|
282
|
-
const
|
|
283
|
-
console.log('\n%sPerfil%s · actualizado %s\n', B, Z,
|
|
315
|
+
const when = me.updatedAt ? new Date(me.updatedAt).toLocaleString() : '—'
|
|
316
|
+
console.log('\n%sPerfil%s · actualizado %s\n', B, Z, when)
|
|
284
317
|
console.log(' nombre : %s', me.nickname || '(sin nombre)')
|
|
285
318
|
console.log(' foto : %s', me.avatar
|
|
286
319
|
? `sí · ${me.avatar.type || 'desconocido'} · ${(me.avatar.bytes / 1024).toFixed(1)} KB`
|
|
@@ -289,17 +322,17 @@ async function cmdMe () {
|
|
|
289
322
|
// Los campos estándar. `visible` es del usuario: teléfono y dirección nacen ocultos.
|
|
290
323
|
const STD = [['nombres', 'nombres'], ['apellidos', 'apellidos'], ['email', 'correo'],
|
|
291
324
|
['telefono', 'teléfono'], ['direccion', 'dirección']]
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
325
|
+
const filled = STD.filter(([k]) => me[k])
|
|
326
|
+
if (filled.length) {
|
|
294
327
|
console.log('')
|
|
295
|
-
for (const [k,
|
|
296
|
-
console.log(' %s: %s%s',
|
|
328
|
+
for (const [k, label] of filled) {
|
|
329
|
+
console.log(' %s: %s%s', label.padEnd(12), me[k], me[k + 'Visible'] === false ? ' (oculto)' : '')
|
|
297
330
|
}
|
|
298
331
|
}
|
|
299
|
-
for (const [
|
|
300
|
-
if (!Array.isArray(
|
|
301
|
-
console.log('\n %s:',
|
|
302
|
-
for (const x of
|
|
332
|
+
for (const [title, list] of [['Enlaces', me.links], ['Otros datos', me.fields]]) {
|
|
333
|
+
if (!Array.isArray(list) || !list.length) continue
|
|
334
|
+
console.log('\n %s:', title)
|
|
335
|
+
for (const x of list) console.log(' %s %s%s', (x.type || x.label || '').padEnd(12), x.value, x.visible === false ? ' (oculto)' : '')
|
|
303
336
|
}
|
|
304
337
|
|
|
305
338
|
console.log('')
|
|
@@ -311,30 +344,31 @@ async function cmdMe () {
|
|
|
311
344
|
*/
|
|
312
345
|
async function cmdMembers () {
|
|
313
346
|
const s = requireDaemon()
|
|
314
|
-
const
|
|
315
|
-
try { fs.rmSync(
|
|
347
|
+
const recordFile = path.join(dataDir(), 'acta.json')
|
|
348
|
+
try { fs.rmSync(recordFile, { force: true }) } catch (_) {}
|
|
316
349
|
writeReq('dump-request.json', {})
|
|
317
|
-
|
|
318
|
-
let
|
|
319
|
-
for (let i = 0; i < 50; i++) { await sleep(100); const a = readJson(
|
|
320
|
-
if (!
|
|
321
|
-
|
|
350
|
+
sendSignal(s.pid, 'SIGUSR2')
|
|
351
|
+
let record = null
|
|
352
|
+
for (let i = 0; i < 50; i++) { await sleep(100); const a = readJson(recordFile, null); if (a?.at) { record = a; break } }
|
|
353
|
+
if (!record) { console.error('El daemon no respondió.'); process.exit(1) }
|
|
354
|
+
assertOpen(record)
|
|
355
|
+
if (!record.members?.length) { console.log('Este perfil todavía no tiene acta.'); return }
|
|
322
356
|
|
|
323
357
|
const CAP = { sign: 'firma', store: 'guarda', read: 'lee', secrets: 'lee sus claves', admin: `${B}administra el perfil${Z}` }
|
|
324
358
|
// El nombre del perfil es una pubkey JWK. Recortarla no la hace legible: la deja
|
|
325
359
|
// pareciendo un error (`{"key_ops":["verify"],"e…`). Se muestra su huella corta, la
|
|
326
360
|
// misma que se enseña al emparejar y en la lista de miembros.
|
|
327
|
-
const
|
|
328
|
-
console.log('\n%sPerfil%s %s · acta #%d\n', B, Z,
|
|
329
|
-
for (const m of
|
|
330
|
-
const
|
|
331
|
-
const
|
|
361
|
+
const profileId = await deviceIdOf(record.profileId).catch(() => '????-????')
|
|
362
|
+
console.log('\n%sPerfil%s %s · acta #%d\n', B, Z, profileId, record.seq)
|
|
363
|
+
for (const m of record.members) {
|
|
364
|
+
const who = m.label || m.id
|
|
365
|
+
const marks = [
|
|
332
366
|
m.isMaster ? `${B}Master${Z}` : null,
|
|
333
367
|
m.isMe ? 'este dispositivo' : null,
|
|
334
368
|
m.cn ? `servicio «${m.cn}»` : null
|
|
335
369
|
].filter(Boolean)
|
|
336
370
|
const caps = m.caps.length ? m.caps.map((c) => CAP[c] || c).join(', ') : '(sin permisos)'
|
|
337
|
-
console.log(' %s %s%s\n %s', m.id,
|
|
371
|
+
console.log(' %s %s%s\n %s', m.id, who, marks.length ? ' [' + marks.join(' · ') + ']' : '', caps)
|
|
338
372
|
}
|
|
339
373
|
console.log('\n Cambiar permisos: dotrino-vault caps <ID> +firma | -firma | +guarda | -guarda | +lee | -lee | +administra')
|
|
340
374
|
console.log(' «Administra» deja conectar y quitar dispositivos desde ese aparato, sin venir aquí.')
|
|
@@ -350,55 +384,56 @@ async function cmdMembers () {
|
|
|
350
384
|
* tener que revocar y volver a emparejar.
|
|
351
385
|
*/
|
|
352
386
|
async function cmdLabel (args = []) {
|
|
353
|
-
const [id, ...
|
|
354
|
-
const
|
|
355
|
-
if (!id || !
|
|
387
|
+
const [id, ...rest] = args
|
|
388
|
+
const name = rest.join(' ').trim()
|
|
389
|
+
if (!id || !name) {
|
|
356
390
|
console.error('uso: dotrino-vault label <ID> <nombre> (p.ej. label AB12-CD34 "Teléfono de casa")')
|
|
357
391
|
process.exit(2)
|
|
358
392
|
}
|
|
359
|
-
const m = await
|
|
360
|
-
writeReq('label-request.json', { pub: m.pub, label:
|
|
361
|
-
|
|
362
|
-
console.log('Listo: %s ahora se llama «%s». Compruébalo con: dotrino-vault members', m.id,
|
|
393
|
+
const m = await findMember(id)
|
|
394
|
+
writeReq('label-request.json', { pub: m.pub, label: name })
|
|
395
|
+
sendSignal(requireDaemon().pid, 'SIGUSR2')
|
|
396
|
+
console.log('Listo: %s ahora se llama «%s». Compruébalo con: dotrino-vault members', m.id, name.slice(0, 60))
|
|
363
397
|
}
|
|
364
398
|
|
|
365
399
|
/** Busca un miembro del acta por su identificador (AB12-CD34) o se rinde con un mensaje claro. */
|
|
366
|
-
async function
|
|
400
|
+
async function findMember (id) {
|
|
367
401
|
const s = requireDaemon()
|
|
368
|
-
const
|
|
369
|
-
try { fs.rmSync(
|
|
402
|
+
const recordFile = path.join(dataDir(), 'acta.json')
|
|
403
|
+
try { fs.rmSync(recordFile, { force: true }) } catch (_) {}
|
|
370
404
|
writeReq('dump-request.json', {})
|
|
371
|
-
|
|
372
|
-
let
|
|
373
|
-
for (let i = 0; i < 50; i++) { await sleep(100); const a = readJson(
|
|
374
|
-
|
|
405
|
+
sendSignal(s.pid, 'SIGUSR2')
|
|
406
|
+
let record = null
|
|
407
|
+
for (let i = 0; i < 50; i++) { await sleep(100); const a = readJson(recordFile, null); if (a?.at) { record = a; break } }
|
|
408
|
+
assertOpen(record)
|
|
409
|
+
const m = record?.members?.find((x) => x.id === String(id).toUpperCase())
|
|
375
410
|
if (!m) { console.error('No hay ningún dispositivo con ese identificador. Míralos con: dotrino-vault members'); process.exit(1) }
|
|
376
411
|
return m
|
|
377
412
|
}
|
|
378
413
|
|
|
379
414
|
/** `dotrino-vault caps <ID> ±permiso` — cambia lo que puede hacer un dispositivo. */
|
|
380
415
|
async function cmdCaps (args = []) {
|
|
381
|
-
const [id, ...
|
|
382
|
-
if (!id || !
|
|
416
|
+
const [id, ...changes] = args
|
|
417
|
+
if (!id || !changes.length) {
|
|
383
418
|
console.error('uso: dotrino-vault caps <ID> +firma|-firma|+guarda|-guarda|+lee|-lee|+administra|-administra')
|
|
384
419
|
process.exit(2)
|
|
385
420
|
}
|
|
386
|
-
const
|
|
421
|
+
const CAP_BY_WORD = {
|
|
387
422
|
firma: 'sign', guarda: 'store', lee: 'read', administra: 'admin',
|
|
388
423
|
sign: 'sign', store: 'store', read: 'read', admin: 'admin'
|
|
389
424
|
}
|
|
390
425
|
const s = requireDaemon()
|
|
391
|
-
const m = await
|
|
426
|
+
const m = await findMember(id)
|
|
392
427
|
|
|
393
428
|
const caps = new Set(m.caps)
|
|
394
|
-
for (const c of
|
|
395
|
-
const
|
|
396
|
-
const cap =
|
|
397
|
-
if (!cap || (
|
|
398
|
-
if (
|
|
429
|
+
for (const c of changes) {
|
|
430
|
+
const sign = c[0]
|
|
431
|
+
const cap = CAP_BY_WORD[c.slice(1).toLowerCase()]
|
|
432
|
+
if (!cap || (sign !== '+' && sign !== '-')) { console.error('permiso no reconocido: %s', c); process.exit(2) }
|
|
433
|
+
if (sign === '+') caps.add(cap); else caps.delete(cap)
|
|
399
434
|
}
|
|
400
435
|
writeReq('caps-request.json', { pub: m.pub, caps: [...caps] })
|
|
401
|
-
|
|
436
|
+
sendSignal(s.pid, 'SIGUSR2')
|
|
402
437
|
console.log('Listo. Compruébalo con: dotrino-vault members')
|
|
403
438
|
}
|
|
404
439
|
|
|
@@ -406,25 +441,26 @@ async function cmdDevices () {
|
|
|
406
441
|
const s = requireDaemon()
|
|
407
442
|
try { fs.rmSync(devFile, { force: true }) } catch (_) {}
|
|
408
443
|
writeReq('dump-request.json', {}) // de qué perfil queremos los dispositivos
|
|
409
|
-
|
|
444
|
+
sendSignal(s.pid, 'SIGUSR2')
|
|
410
445
|
let snap = null
|
|
411
446
|
for (let i = 0; i < 50; i++) { await sleep(100); const d = readJson(devFile, null); if (d?.at) { snap = d; break } }
|
|
412
447
|
if (!snap) { console.error('El daemon no respondió.'); process.exit(1) }
|
|
448
|
+
assertOpen(snap)
|
|
413
449
|
const revoked = snap.revoked || []
|
|
414
|
-
const
|
|
450
|
+
const revokedSet = new Set(revoked.map((r) => r?.nonce || r))
|
|
415
451
|
// UN APARATO, UNA LÍNEA. El daemon lleva la cuenta por CERTIFICADO —correcto para él,
|
|
416
452
|
// porque revocar es revocar un papel—, pero renovar emite uno nuevo cada 30 días: un
|
|
417
453
|
// aparato de un año salía doce veces, y los ya retirados seguían contando como
|
|
418
454
|
// enrolados. Se agrupa por llave y se dice cuántos certificados tiene.
|
|
419
|
-
const
|
|
455
|
+
const byKey = new Map()
|
|
420
456
|
for (const d of (snap.issued || snap.active || snap.delegations || [])) {
|
|
421
|
-
if (d.revokedAt ||
|
|
422
|
-
const
|
|
423
|
-
const y =
|
|
424
|
-
if (!y)
|
|
457
|
+
if (d.revokedAt || revokedSet.has(d.nonce)) continue
|
|
458
|
+
const key = d.sub || d.nonce
|
|
459
|
+
const y = byKey.get(key)
|
|
460
|
+
if (!y) byKey.set(key, { ...d, certs: 1 })
|
|
425
461
|
else { y.certs++; if ((d.exp || 0) > (y.exp || 0)) Object.assign(y, { ...d, certs: y.certs }) }
|
|
426
462
|
}
|
|
427
|
-
const active = [...
|
|
463
|
+
const active = [...byKey.values()]
|
|
428
464
|
console.log('Dispositivos enrolados: %d', active.length)
|
|
429
465
|
for (const d of active) {
|
|
430
466
|
const did = d.sub ? await deviceIdOf(d.sub) : '????-????'
|
|
@@ -454,14 +490,14 @@ async function cmdRevoke (arg) {
|
|
|
454
490
|
const esId = /^[0-9a-f]{4}-?[0-9a-f]{4}$/i.test(arg)
|
|
455
491
|
const s = requireDaemon()
|
|
456
492
|
if (esId) {
|
|
457
|
-
const m = await
|
|
493
|
+
const m = await findMember(arg.toUpperCase().includes('-') ? arg.toUpperCase() : arg.toUpperCase().replace(/(.{4})(.{4})/, '$1-$2'))
|
|
458
494
|
writeReq('revoke-request.json', { sub: m.pub })
|
|
459
|
-
|
|
495
|
+
sendSignal(s.pid, 'SIGUSR2')
|
|
460
496
|
console.log('Quitado %s (todos sus certificados). Se autoborrará al reconectar. Verifica: dotrino-vault devices', m.id)
|
|
461
497
|
return
|
|
462
498
|
}
|
|
463
499
|
writeReq('revoke-request.json', { nonce: arg })
|
|
464
|
-
|
|
500
|
+
sendSignal(s.pid, 'SIGUSR2')
|
|
465
501
|
console.log('Revocación enviada para nonce=%s. El dispositivo se autoborrará al reconectar. Verifica: dotrino-vault devices', arg)
|
|
466
502
|
}
|
|
467
503
|
|
|
@@ -478,6 +514,10 @@ function profileDir () {
|
|
|
478
514
|
? list.find((x) => x.id === PROFILE || (x.name || '').toLowerCase() === ref)
|
|
479
515
|
: (list.find((x) => x.current) || list[0])
|
|
480
516
|
if (!p) { console.error('el perfil no existe: %s', PROFILE); process.exit(1) }
|
|
517
|
+
// El candado también aquí: esto es la única puerta que lee el directorio del perfil sin
|
|
518
|
+
// pasar por el daemon (la bitácora), y sin esta línea `activity` seguía contando quién
|
|
519
|
+
// firmó y cuándo con la bóveda cerrada.
|
|
520
|
+
assertOpen(p)
|
|
481
521
|
return path.join(dir, 'p', p.id)
|
|
482
522
|
}
|
|
483
523
|
|
|
@@ -500,43 +540,265 @@ function cmdActivity (n = 30) {
|
|
|
500
540
|
}
|
|
501
541
|
}
|
|
502
542
|
|
|
503
|
-
//
|
|
504
|
-
// leen los SERVICIOS enrolados con `pair --service <ns>`. Nunca se listan valores.
|
|
543
|
+
// Variables de entorno de los servicios: se cargan aquí (el dueño, en el PC del vault) y
|
|
544
|
+
// las leen los SERVICIOS enrolados con `pair --service <ns>`. Nunca se listan valores.
|
|
545
|
+
//
|
|
546
|
+
// DOS CAJONES: las del SCOPE (`secret set <ns> …`) las comparten todos los aparatos que
|
|
547
|
+
// sirven ese namespace; las del APARATO (`secret device set <ID> …`) las lee solo ese
|
|
548
|
+
// aparato y PISAN a las del scope con el mismo nombre. Ahí va lo que cambia de máquina a
|
|
549
|
+
// máquina (el puerto, la URL pública) sin tener que partir el ns en uno por servidor.
|
|
550
|
+
/**
|
|
551
|
+
* `KEY=valor KEY2=valor2` — la forma de CARGAR VARIAS de una vez. Devuelve `null` si no
|
|
552
|
+
* son todos pares, para que la forma clásica de tres argumentos (`set ns CLAVE valor`,
|
|
553
|
+
* donde el valor puede llevar espacios y hasta un `=`) siga funcionando igual.
|
|
554
|
+
*/
|
|
555
|
+
function asPairs (args) {
|
|
556
|
+
if (!args.length) return null
|
|
557
|
+
const out = []
|
|
558
|
+
for (const a of args) {
|
|
559
|
+
const m = PAIR_RE.exec(a)
|
|
560
|
+
if (!m) return null
|
|
561
|
+
out.push({ op: 'set', key: m[1], value: m[2] })
|
|
562
|
+
}
|
|
563
|
+
return out
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Un `.env` con un problema no se carga A MEDIAS: se dice qué línea y no se escribe nada. */
|
|
567
|
+
function abortEnv (errors) {
|
|
568
|
+
console.error('%sNo se cargó nada%s:', R, Z)
|
|
569
|
+
for (const e of errors) console.error(' · %s', envErrorText(e))
|
|
570
|
+
process.exit(2)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Los códigos del lector de `.env`, en la lengua del CLI. */
|
|
574
|
+
function envErrorText (e) {
|
|
575
|
+
if (e.code === 'shape') return `línea ${e.line}: no tiene la forma CLAVE=valor`
|
|
576
|
+
if (e.code === 'key') return `línea ${e.line}: «${e.key}» va en MAYÚSCULAS_CON_GUION_BAJO`
|
|
577
|
+
if (e.code === 'novalue') return `línea ${e.line}: ${e.key} no tiene valor (para quitarla: secret rm)`
|
|
578
|
+
if (e.code === 'dup') return `línea ${e.line}: ${e.key} ya venía en la línea ${e.first}`
|
|
579
|
+
return 'no hay ninguna variable que cargar'
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** El problema de una variable, en español, o `null`. Las reglas son las del cajón. */
|
|
583
|
+
function problemWith (key, value) {
|
|
584
|
+
try { assertVar(key, value); return null } catch (e) {
|
|
585
|
+
if (/invalid key/.test(e.message)) return 'el nombre va en MAYÚSCULAS_CON_GUION_BAJO (p. ej. TURN_KEY_ID)'
|
|
586
|
+
if (/non-empty/.test(e.message)) return 'no tiene valor (para quitarla: secret rm)'
|
|
587
|
+
if (/too long/.test(e.message)) return 'el valor es demasiado largo'
|
|
588
|
+
return e.message
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
505
592
|
async function cmdSecret (rest) {
|
|
506
|
-
|
|
593
|
+
// --public / --private: si el VALOR puede salir de esta máquina hacia la consola remota.
|
|
594
|
+
// Se sacan de la línea antes de partirla, para que puedan ir en cualquier posición y no
|
|
595
|
+
// se cuelen dentro del valor (que es lo último y puede llevar espacios).
|
|
596
|
+
let isPublic
|
|
597
|
+
const rest2 = rest.filter((a) => {
|
|
598
|
+
if (a === '--public') { isPublic = true; return false }
|
|
599
|
+
if (a === '--private') { isPublic = false; return false }
|
|
600
|
+
return true
|
|
601
|
+
})
|
|
602
|
+
const [sub, ...args] = rest2
|
|
507
603
|
const s = requireDaemon()
|
|
508
604
|
const secretsListFile = path.join(dir, 'secrets-list.json')
|
|
509
605
|
const signalAndWaitList = async () => {
|
|
510
606
|
try { fs.rmSync(secretsListFile, { force: true }) } catch (_) {}
|
|
511
607
|
writeReq('dump-request.json', {}) // de qué perfil son los secretos
|
|
512
|
-
|
|
513
|
-
for (let i = 0; i < 50; i++) {
|
|
608
|
+
sendSignal(s.pid, 'SIGUSR2')
|
|
609
|
+
for (let i = 0; i < 50; i++) {
|
|
610
|
+
await sleep(100)
|
|
611
|
+
const d = readJson(secretsListFile, null)
|
|
612
|
+
// El volcado lleva el valor de las públicas: se borra en cuanto se tiene, para que no
|
|
613
|
+
// se quede esperando en el disco a que copien la carpeta (ver `daemon.js`).
|
|
614
|
+
if (d?.at) { try { fs.rmSync(secretsListFile, { force: true }) } catch (_) {} ; return assertOpen(d) }
|
|
615
|
+
}
|
|
514
616
|
console.error('El daemon no respondió.'); process.exit(1)
|
|
515
617
|
}
|
|
618
|
+
const USAGE = [
|
|
619
|
+
'uso: dotrino-vault secret set <ns> <CLAVE> <valor> [--public|--private]',
|
|
620
|
+
' (la comparten todos los aparatos del ns)',
|
|
621
|
+
' dotrino-vault secret set <ns> CLAVE=valor [CLAVE2=valor2 …] varias DE UNA VEZ',
|
|
622
|
+
' dotrino-vault secret import <ns> [archivo.env] desde un .env (o stdin)',
|
|
623
|
+
' dotrino-vault secret rm <ns> <CLAVE>',
|
|
624
|
+
' dotrino-vault secret device set <ID> <CLAVE> <valor> [--public|--private]',
|
|
625
|
+
' (solo la lee ese aparato, y pisa a la del ns)',
|
|
626
|
+
' dotrino-vault secret device set <ID> CLAVE=valor [CLAVE2=valor2 …]',
|
|
627
|
+
' dotrino-vault secret device import <ID> [archivo.env]',
|
|
628
|
+
' dotrino-vault secret device rm <ID> <CLAVE>',
|
|
629
|
+
' dotrino-vault secret list',
|
|
630
|
+
'',
|
|
631
|
+
'CARGA LA CONFIGURACIÓN DE UN SERVICIO DE UNA VEZ (`set` con varios pares, o `import`):',
|
|
632
|
+
'la bóveda la aplica entera y avisa UNA sola vez. De una en una, cada variable es un',
|
|
633
|
+
'cambio de configuración y el servicio se reinicia a media carga.',
|
|
634
|
+
'',
|
|
635
|
+
'Pública o privada dice UNA cosa: si el VALOR puede salir de esta máquina hacia la',
|
|
636
|
+
'consola remota (vault.dotrino.com). Se nace privada. El servicio recibe las dos igual.'
|
|
637
|
+
].join('\n')
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Manda un grupo entero al daemon y comprueba que llegó completo.
|
|
641
|
+
*
|
|
642
|
+
* Se valida TODO aquí antes de escribir nada: si una variable del archivo está mal, no
|
|
643
|
+
* se carga ninguna. Media configuración aplicada es peor que ninguna, porque el
|
|
644
|
+
* servicio arranca con ella y parece que funcionó.
|
|
645
|
+
*/
|
|
646
|
+
const sendBatch = async ({ ns = null, pub = null, items, where }) => {
|
|
647
|
+
const bad = items.map((it) => [it.key, problemWith(it.key, it.value)]).filter(([, p]) => p)
|
|
648
|
+
if (bad.length) {
|
|
649
|
+
console.error('%sNo se cargó nada%s. Revisa:', R, Z)
|
|
650
|
+
for (const [key, p] of bad) console.error(' · %s: %s', key, p)
|
|
651
|
+
process.exit(2)
|
|
652
|
+
}
|
|
653
|
+
const withVisibility = items.map((it) => (isPublic === undefined ? it : { ...it, public: isPublic }))
|
|
654
|
+
writeReq('secret-request.json', pub ? { op: 'batch', pub, items: withVisibility } : { op: 'batch', ns, items: withVisibility })
|
|
655
|
+
const d = await signalAndWaitList()
|
|
656
|
+
const list = pub
|
|
657
|
+
? ((Array.isArray(d.dev) ? d.dev : []).find((x) => x.pub === pub)?.keys || [])
|
|
658
|
+
: (d.ns?.[ns] || [])
|
|
659
|
+
const missing = items.filter((it) => !has(list, it.key)).map((it) => it.key)
|
|
660
|
+
if (missing.length) {
|
|
661
|
+
console.error('El daemon no guardó: %s (revisa: dotrino-vault logs)', missing.join(', '))
|
|
662
|
+
process.exit(1)
|
|
663
|
+
}
|
|
664
|
+
console.log('%d variables guardadas en %s%s%s%s', items.length, B, where, Z,
|
|
665
|
+
isPublic === undefined ? '' : isPublic ? ' (públicas)' : ' (privadas)')
|
|
666
|
+
console.log('Un solo aviso de cambio: el servicio se reinicia una vez, con todo puesto.')
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** El texto del `.env`: de un archivo, o de la entrada estándar si no se da ninguno. */
|
|
670
|
+
const readEnvText = (file) => {
|
|
671
|
+
if (file) {
|
|
672
|
+
try { return fs.readFileSync(file, 'utf8') } catch (e) {
|
|
673
|
+
console.error('No se pudo leer %s: %s', file, e.message); process.exit(1)
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (process.stdin.isTTY) {
|
|
677
|
+
console.error('%s\n\nimport necesita un archivo, o el .env por la entrada estándar:', USAGE)
|
|
678
|
+
console.error(' dotrino-vault secret import proxy .env')
|
|
679
|
+
console.error(' cat .env | dotrino-vault secret import proxy')
|
|
680
|
+
process.exit(2)
|
|
681
|
+
}
|
|
682
|
+
return fs.readFileSync(0, 'utf8')
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Una variable en la lista: su nombre y su valor. La PÚBLICA enseña el suyo (pública
|
|
687
|
+
* quiere decir que ese valor puede salir de esta máquina: taparlo aquí, delante de su
|
|
688
|
+
* dueño, era lo único que la marca no significaba). La privada no se muestra.
|
|
689
|
+
*/
|
|
690
|
+
const printVar = (k) => console.log(' · %s %s', k.key, k.public ? `${k.value ?? ''} (pública)` : '••••••')
|
|
691
|
+
const has = (list, key) => (list || []).some((x) => x.key === key)
|
|
692
|
+
|
|
516
693
|
if (sub === 'list') {
|
|
517
694
|
const d = await signalAndWaitList()
|
|
518
695
|
const names = d.ns || {}
|
|
519
696
|
const nss = Object.keys(names)
|
|
520
|
-
|
|
697
|
+
const dev = Array.isArray(d.dev) ? d.dev : []
|
|
698
|
+
if (!nss.length && !dev.length) {
|
|
699
|
+
console.log('No hay variables guardadas. Agrega una: dotrino-vault secret set <ns> <CLAVE> <valor>')
|
|
700
|
+
return
|
|
701
|
+
}
|
|
702
|
+
if (nss.length) console.log('\n%sPor scope%s (las comparten todos los aparatos del perfil)\n', B, Z)
|
|
521
703
|
for (const n of nss) {
|
|
522
704
|
console.log('%s%s%s (scope vault:secrets:%s)', B, n, Z, n)
|
|
523
|
-
for (const k of names[n])
|
|
705
|
+
for (const k of names[n]) printVar(k)
|
|
706
|
+
}
|
|
707
|
+
if (dev.length) console.log('\n%sPor aparato%s (solo las lee ese aparato; pisan a las del scope)\n', B, Z)
|
|
708
|
+
for (const x of dev) {
|
|
709
|
+
const who = [x.label, x.cn ? `servicio «${x.cn}»` : null, x.orphan ? 'YA NO ESTÁ EN EL ACTA' : null].filter(Boolean).join(' · ')
|
|
710
|
+
console.log('%s%s%s%s', B, x.id, Z, who ? ' ' + who : '')
|
|
711
|
+
for (const k of x.keys) printVar(k)
|
|
524
712
|
}
|
|
713
|
+
console.log('\n(pública) = su valor se puede ver desde la consola remota. Las demás no salen de aquí.\n')
|
|
525
714
|
return
|
|
526
715
|
}
|
|
527
|
-
|
|
716
|
+
|
|
717
|
+
// Por APARATO: `secret device set|rm|visibility <ID> <CLAVE> [valor|public|private]`.
|
|
718
|
+
if (sub === 'device') {
|
|
719
|
+
const [op, id, key, ...valueParts] = args
|
|
528
720
|
const value = valueParts.join(' ')
|
|
529
|
-
|
|
530
|
-
|
|
721
|
+
const ops = ['set', 'rm', 'visibility', 'import']
|
|
722
|
+
const asGroup = op === 'import' || (op === 'set' && !!asPairs(args.slice(2)))
|
|
723
|
+
if (!ops.includes(op) || !id || (!asGroup && (!key || (op === 'set' && !value)))) { console.error(USAGE); process.exit(2) }
|
|
724
|
+
const m = await findMember(id)
|
|
725
|
+
// Se avisa aquí, con nombre y apellido, en vez de dejar que el daemon lo rechace y la
|
|
726
|
+
// CLI diga «no aplicó el cambio»: quien escribe esto quiere saber POR QUÉ no vale.
|
|
727
|
+
if (!m.cn) {
|
|
728
|
+
console.error('%s no es un servicio, y solo los servicios leen variables.', m.id)
|
|
729
|
+
console.error('Empareja el servicio con: dotrino-vault pair --service <ns>')
|
|
730
|
+
process.exit(1)
|
|
531
731
|
}
|
|
532
|
-
|
|
732
|
+
if (op === 'import') {
|
|
733
|
+
const { items, errors } = parseEnvText(readEnvText(args[2]))
|
|
734
|
+
if (errors.length) return abortEnv(errors)
|
|
735
|
+
return sendBatch({ pub: m.pub, items, where: m.id })
|
|
736
|
+
}
|
|
737
|
+
if (asGroup) return sendBatch({ pub: m.pub, items: asPairs(args.slice(2)), where: m.id })
|
|
738
|
+
if (key && PAIR_RE.test(key) && args.length > 3) {
|
|
739
|
+
console.error('%s\n\nO todos los argumentos son CLAVE=valor, o es una sola variable.', USAGE)
|
|
740
|
+
process.exit(2)
|
|
741
|
+
}
|
|
742
|
+
const req = op === 'set'
|
|
743
|
+
? { op: 'dev-set', pub: m.pub, key, value, ...(isPublic === undefined ? {} : { public: isPublic }) }
|
|
744
|
+
: op === 'rm'
|
|
745
|
+
? { op: 'dev-rm', pub: m.pub, key }
|
|
746
|
+
: { op: 'dev-vis', pub: m.pub, key, public: wantsPublic(value, USAGE) }
|
|
747
|
+
writeReq('secret-request.json', req)
|
|
533
748
|
const d = await signalAndWaitList()
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
749
|
+
const keys = (Array.isArray(d.dev) ? d.dev : []).find((x) => x.pub === m.pub)?.keys || []
|
|
750
|
+
const ok = op === 'rm' ? !has(keys, key) : has(keys, key)
|
|
751
|
+
if (!ok) { console.error('El daemon no aplicó el cambio (revisa: dotrino-vault logs)'); process.exit(1) }
|
|
752
|
+
if (op === 'rm') console.log('Variable borrada: %s/%s', m.id, key)
|
|
753
|
+
else console.log('Variable guardada: %s/%s%s', m.id, key, (keys.find((x) => x.key === key)?.public) ? ' (pública)' : '')
|
|
537
754
|
return
|
|
538
755
|
}
|
|
539
|
-
|
|
756
|
+
|
|
757
|
+
// Desde un `.env`: el caso real de estrenar un servicio, y el que de una en una
|
|
758
|
+
// reiniciaba al agente una vez por variable.
|
|
759
|
+
if (sub === 'import') {
|
|
760
|
+
const [ns, file] = args
|
|
761
|
+
if (!ns) { console.error(USAGE); process.exit(2) }
|
|
762
|
+
const { items, errors } = parseEnvText(readEnvText(file))
|
|
763
|
+
if (errors.length) return abortEnv(errors)
|
|
764
|
+
return sendBatch({ ns, items, where: ns })
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (sub === 'set' || sub === 'rm' || sub === 'visibility') {
|
|
768
|
+
const [ns, key, ...valueParts] = args
|
|
769
|
+
const value = valueParts.join(' ')
|
|
770
|
+
// `set <ns> CLAVE=valor CLAVE2=valor2` — varias de una vez, un solo aviso.
|
|
771
|
+
const pairs = sub === 'set' ? asPairs(args.slice(1)) : null
|
|
772
|
+
if (ns && pairs) return sendBatch({ ns, items: pairs, where: ns })
|
|
773
|
+
// Mezclar las dos formas (`K1=v1 CLAVE valor`) no es ninguna de las dos: mejor
|
|
774
|
+
// decirlo que guardar una variable llamada «K1=v1».
|
|
775
|
+
if (sub === 'set' && key && PAIR_RE.test(key) && args.length > 2) {
|
|
776
|
+
console.error('%s\n\nO todos los argumentos son CLAVE=valor, o es una sola variable: set <ns> <CLAVE> <valor>.', USAGE)
|
|
777
|
+
process.exit(2)
|
|
778
|
+
}
|
|
779
|
+
if (!ns || !key || (sub === 'set' && !value)) { console.error(USAGE); process.exit(2) }
|
|
780
|
+
const req = sub === 'set'
|
|
781
|
+
? { op: 'set', ns, key, value, ...(isPublic === undefined ? {} : { public: isPublic }) }
|
|
782
|
+
: sub === 'rm'
|
|
783
|
+
? { op: 'rm', ns, key }
|
|
784
|
+
: { op: 'vis', ns, key, public: wantsPublic(value, USAGE) }
|
|
785
|
+
writeReq('secret-request.json', req)
|
|
786
|
+
const d = await signalAndWaitList()
|
|
787
|
+
const list = d.ns?.[ns] || []
|
|
788
|
+
const ok = sub === 'rm' ? !has(list, key) : has(list, key)
|
|
789
|
+
if (!ok) { console.error('El daemon no aplicó el cambio (revisa: dotrino-vault logs)'); process.exit(1) }
|
|
790
|
+
if (sub === 'rm') console.log('Secreto borrado: %s/%s', ns, key)
|
|
791
|
+
else console.log('Secreto guardado: %s/%s%s', ns, key, (list.find((x) => x.key === key)?.public) ? ' (pública)' : '')
|
|
792
|
+
return
|
|
793
|
+
}
|
|
794
|
+
console.error(USAGE); process.exit(2)
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** `visibility … public|private` — el único argumento que acepta, y sin adivinar. */
|
|
798
|
+
function wantsPublic (word, usage) {
|
|
799
|
+
if (word === 'public') return true
|
|
800
|
+
if (word === 'private') return false
|
|
801
|
+
console.error('%s\n\nvisibility acepta «public» o «private».', usage); process.exit(2)
|
|
540
802
|
}
|
|
541
803
|
|
|
542
804
|
/**
|
|
@@ -546,7 +808,7 @@ async function cmdSecret (rest) {
|
|
|
546
808
|
function askPassword (prompt) {
|
|
547
809
|
return new Promise((resolve, reject) => {
|
|
548
810
|
const stdin = process.stdin
|
|
549
|
-
if (!stdin.isTTY) return reject(new Error('
|
|
811
|
+
if (!stdin.isTTY) return reject(new Error('a terminal is required to type the password'))
|
|
550
812
|
process.stdout.write(prompt)
|
|
551
813
|
stdin.setRawMode(true); stdin.resume(); stdin.setEncoding('utf8')
|
|
552
814
|
let buf = ''
|
|
@@ -558,7 +820,7 @@ function askPassword (prompt) {
|
|
|
558
820
|
const onData = (ch) => {
|
|
559
821
|
for (const c of ch) {
|
|
560
822
|
if (c === '\n' || c === '\r' || c === '\u0004') return done(null, buf) // Enter / Ctrl-D
|
|
561
|
-
if (c === '\u0003') return done(new Error('
|
|
823
|
+
if (c === '\u0003') return done(new Error('cancelled')) // Ctrl-C
|
|
562
824
|
if (c === '\u007f' || c === '\b') { buf = buf.slice(0, -1); continue } // borrar
|
|
563
825
|
buf += c
|
|
564
826
|
}
|
|
@@ -576,7 +838,7 @@ async function profileRequest (op, extra = {}) {
|
|
|
576
838
|
const s = requireDaemon()
|
|
577
839
|
try { fs.rmSync(profilesFile, { force: true }) } catch (_) {}
|
|
578
840
|
writeReq('profile-request.json', { op, ...extra })
|
|
579
|
-
|
|
841
|
+
sendSignal(s.pid, 'SIGUSR2')
|
|
580
842
|
for (let i = 0; i < 100; i++) {
|
|
581
843
|
await sleep(100)
|
|
582
844
|
const d = readJson(profilesFile, null)
|
|
@@ -586,7 +848,14 @@ async function profileRequest (op, extra = {}) {
|
|
|
586
848
|
}
|
|
587
849
|
|
|
588
850
|
function reportProfiles (d) {
|
|
589
|
-
if (d.error) {
|
|
851
|
+
if (d.error) {
|
|
852
|
+
// Los dos rechazos del candado se dicen con palabras y con el dato que hace falta; el
|
|
853
|
+
// resto se reenvía tal cual (son diagnósticos del servicio).
|
|
854
|
+
if (d.code === 'WRONG_PASSWORD') console.error('Contraseña incorrecta%s.', d.tries ? ` — van ${d.tries} intentos fallidos` : '')
|
|
855
|
+
else if (d.code === 'TOO_MANY_TRIES') console.error('Demasiados intentos: espera %s s antes de volver a probar.', d.waitSec || '?')
|
|
856
|
+
else console.error('%s', d.error)
|
|
857
|
+
process.exit(1)
|
|
858
|
+
}
|
|
590
859
|
if (d.done) console.log('%s', d.done)
|
|
591
860
|
return d
|
|
592
861
|
}
|
|
@@ -691,9 +960,25 @@ function help () {
|
|
|
691
960
|
estrena una cuenta VACÍA en este vault y mete ahí al dispositivo
|
|
692
961
|
(sin la bandera entra a la cuenta activa, o a la de --profile)
|
|
693
962
|
pair --service <ns> empareja un SERVICIO (proxy, geo…) con acceso SOLO a sus secretos
|
|
694
|
-
secret set <ns> <CLAVE> <valor>
|
|
695
|
-
|
|
696
|
-
secret
|
|
963
|
+
secret set <ns> <CLAVE> <valor> variable del scope <ns>: la comparten TODOS los
|
|
964
|
+
aparatos del perfil que sirven ese namespace
|
|
965
|
+
secret set <ns> CLAVE=valor CLAVE2=valor2 …
|
|
966
|
+
carga VARIAS de una vez: se aplican juntas y el
|
|
967
|
+
servicio recibe UN solo aviso (se reinicia una vez)
|
|
968
|
+
secret import <ns> [archivo.env] lo mismo desde un .env (o por la entrada estándar)
|
|
969
|
+
secret rm <ns> <CLAVE> borra una variable del scope
|
|
970
|
+
secret device set <ID> <CLAVE> <valor>
|
|
971
|
+
variable de UN aparato: solo la lee él, y pisa a la
|
|
972
|
+
del scope que se llame igual (puerto, URL pública…)
|
|
973
|
+
secret device set <ID> CLAVE=valor …
|
|
974
|
+
secret device import <ID> [archivo.env]
|
|
975
|
+
secret device rm <ID> <CLAVE> borra una variable de ese aparato
|
|
976
|
+
secret list lista los dos cajones: el valor de las públicas,
|
|
977
|
+
tapadas las privadas
|
|
978
|
+
--public | --private (al hacer un set) si el VALOR puede salir de esta
|
|
979
|
+
máquina hacia la consola remota. Se nace privada.
|
|
980
|
+
secret visibility <ns> <CLAVE> public|private cambia eso sin tocar el valor
|
|
981
|
+
secret device visibility <ID> <CLAVE> public|private
|
|
697
982
|
pending muestra el dispositivo pendiente + su código a comparar
|
|
698
983
|
approve <código> aprueba el dispositivo tipeando el código que MUESTRA (el vault no lo sabe)
|
|
699
984
|
reject <deviceId> rechaza un dispositivo pendiente
|