@dotrino/vaultd 0.58.0 → 0.60.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/lib/src/enroll.js +6 -1
- package/package.json +1 -1
- package/src/ctl.js +19 -3
- package/src/tui/app.js +53 -7
- package/src/tui/i18n.js +22 -4
- package/src/vaultControl.js +2 -2
package/lib/src/enroll.js
CHANGED
|
@@ -277,7 +277,12 @@ export function createEnrollDesk ({
|
|
|
277
277
|
pend.continuity = (okC && d.continuity.member === d.dpub) ? d.continuity : null
|
|
278
278
|
}
|
|
279
279
|
pend.from = from // la bóveda NO conoce el código: lo aprende cuando lo tipeas
|
|
280
|
-
|
|
280
|
+
// EL NOMBRE QUE PUSISTE AQUÍ MANDA. El aparato manda el suyo al enrolarse, y hasta
|
|
281
|
+
// ahora pisaba siempre al de la bóveda — como el aparato usa por defecto el apodo del
|
|
282
|
+
// PERFIL, acababas con varios dispositivos llamados igual que tú y sin forma de saber
|
|
283
|
+
// cuál era cuál. Si en `pair` le diste un nombre, ese es el nombre; el del aparato
|
|
284
|
+
// sigue valiendo como propuesta cuando no dijiste nada.
|
|
285
|
+
if (d.label && !pend.label) pend.label = String(d.label).slice(0, 60)
|
|
281
286
|
// Camino A: de qué cuenta estamos hablando. Se guarda para poder comprobar, cuando
|
|
282
287
|
// llegue el acta sellada, que es la que este dispositivo dijo que iba a entregar.
|
|
283
288
|
if (intent === 'adopt' && typeof d.profileId === 'string') pend.profileId = d.profileId
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.0",
|
|
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
|
@@ -205,6 +205,17 @@ async function cmdPair (args = []) {
|
|
|
205
205
|
// --approval: el aparato que entre pedirá tu aprobación (teléfono) en cada petición de
|
|
206
206
|
// claves privadas. Por defecto NO pide; se cambia después con `caps <ID> +permiso`.
|
|
207
207
|
const approval = args.includes('--approval')
|
|
208
|
+
// --name <n>: CÓMO SE VA A LLAMAR el aparato que entre, decidido aquí y antes de nada.
|
|
209
|
+
// Sin esto el nombre lo ponía el propio aparato, que por defecto usa el apodo del
|
|
210
|
+
// PERFIL: acababas con varios dispositivos llamados igual que tú, y para distinguirlos
|
|
211
|
+
// había que renombrarlos después —cuando ya no sabías cuál era cuál—.
|
|
212
|
+
const nIdx = args.indexOf('--name')
|
|
213
|
+
let label = null
|
|
214
|
+
if (nIdx >= 0) {
|
|
215
|
+
const v = args[nIdx + 1]
|
|
216
|
+
if (!v || v.startsWith('-')) { console.error('uso: dotrino-vault pair --name "teléfono de casa"'); process.exit(2) }
|
|
217
|
+
label = v.slice(0, 60)
|
|
218
|
+
}
|
|
208
219
|
// --admin: el aparato que entre por esta invitación podrá ADMINISTRAR (la consola).
|
|
209
220
|
// El QR no lleva nada: es una nota local de la bóveda y el permiso se aplica al aprobar
|
|
210
221
|
// con el código que tecleas aquí. Ahorra el `caps <ID> +administra` de después, que en un
|
|
@@ -258,9 +269,11 @@ async function cmdPair (args = []) {
|
|
|
258
269
|
// meter el dispositivo en una cuenta que ya vive aquí, se ESTRENA una (vacía) y
|
|
259
270
|
// entra a ella; las demás no se tocan. En la TUI esto es una pregunta con sus
|
|
260
271
|
// opciones; en la CLI es una bandera, para que siga sirviendo en un script.
|
|
272
|
+
// `--name` lleva su valor detrás: no puede confundirse con el nombre de la cuenta.
|
|
273
|
+
const consumido = (i) => nIdx >= 0 && i === nIdx + 1
|
|
261
274
|
const naIdx = args.findIndex((a) => a === '--new-account')
|
|
262
275
|
if (naIdx >= 0) {
|
|
263
|
-
const next = args[naIdx + 1]
|
|
276
|
+
const next = consumido(naIdx + 1) ? null : args[naIdx + 1]
|
|
264
277
|
const name = (next && !next.startsWith('-')) ? next : `cuenta ${new Date().toISOString().slice(0, 10)}`
|
|
265
278
|
const d = await profileRequest('add', { name, ...(pairKek ? { kek: pairKek } : {}) })
|
|
266
279
|
if (d.error) { console.error('%s', d.error); process.exit(1) }
|
|
@@ -275,7 +288,7 @@ async function cmdPair (args = []) {
|
|
|
275
288
|
const adIdx = args.findIndex((a) => a === '--adopt')
|
|
276
289
|
const adopt = adIdx >= 0
|
|
277
290
|
if (adopt) {
|
|
278
|
-
const next = args[adIdx + 1]
|
|
291
|
+
const next = consumido(adIdx + 1) ? null : args[adIdx + 1]
|
|
279
292
|
const name = (next && !next.startsWith('-')) ? next : `cuenta del dispositivo`
|
|
280
293
|
const d = await profileRequest('add', { name, adopt: true, ...(pairKek ? { kek: pairKek } : {}) })
|
|
281
294
|
if (d.error) { console.error('%s', d.error); process.exit(1) }
|
|
@@ -298,7 +311,7 @@ async function cmdPair (args = []) {
|
|
|
298
311
|
|
|
299
312
|
// La petición se escribe SIEMPRE (aunque no haya --service): lleva a qué perfil
|
|
300
313
|
// se empareja el dispositivo.
|
|
301
|
-
writeReq('pair-request.json', { ...(service ? { service } : {}), ...(scope ? { scope } : {}), ...(adopt ? { mode: 'adopt' } : {}), ...(approval ? { approval: true } : {}), ...(admin ? { admin: true } : {}) })
|
|
314
|
+
writeReq('pair-request.json', { ...(service ? { service } : {}), ...(scope ? { scope } : {}), ...(adopt ? { mode: 'adopt' } : {}), ...(approval ? { approval: true } : {}), ...(admin ? { admin: true } : {}), ...(label ? { label } : {}) })
|
|
302
315
|
sendSignal(s.pid, 'SIGUSR1')
|
|
303
316
|
|
|
304
317
|
let pair = null
|
|
@@ -1469,6 +1482,9 @@ function help () {
|
|
|
1469
1482
|
estrena una cuenta VACÍA en este vault y mete ahí al dispositivo
|
|
1470
1483
|
(sin la bandera entra a la cuenta activa, o a la de --profile)
|
|
1471
1484
|
pair --service <ns> empareja un SERVICIO (proxy, geo…) con acceso SOLO a sus secretos
|
|
1485
|
+
pair --name <nombre> cómo se llamará el aparato que entre. Sin esto el nombre lo pone
|
|
1486
|
+
ÉL, y por defecto usa el apodo del perfil: acabas con varios
|
|
1487
|
+
dispositivos llamados igual que tú
|
|
1472
1488
|
pair --approval el aparato que entre pedirá tu aprobación (teléfono) al recibir claves
|
|
1473
1489
|
pair --admin el aparato que entre podrá ADMINISTRAR (es lo que es una consola).
|
|
1474
1490
|
El QR no lleva nada: el permiso se aplica al aprobar el código aquí
|
package/src/tui/app.js
CHANGED
|
@@ -127,6 +127,10 @@ function mergeMembersAndCerts (members, issued) {
|
|
|
127
127
|
label: m.label || cert?.label || '',
|
|
128
128
|
isMaster: !!m.isMaster,
|
|
129
129
|
cn: m.cn || null,
|
|
130
|
+
// CUÁNDO ENTRÓ. Venía en el acta y se perdía aquí, así que la lista no podía
|
|
131
|
+
// enseñarlo por mucho que se quisiera. El nombre lo pone el propio aparato y muchas
|
|
132
|
+
// veces no distingue nada: la fecha es lo que deja reconocer cuál es cuál.
|
|
133
|
+
addedAt: m.addedAt || null,
|
|
130
134
|
// El master es la propia bóveda: no tiene (ni necesita) certificado. Un servicio
|
|
131
135
|
// tampoco lleva uno de dispositivo. Marcarlos «sin acceso» sería una alarma falsa.
|
|
132
136
|
noAccess: !cert && !m.isMaster && !m.cn
|
|
@@ -260,6 +264,7 @@ function deviceRows (st, t) {
|
|
|
260
264
|
const label = d.label || t.muted(i.noLabel)
|
|
261
265
|
const vars = devVarsOf(st, d.sub).length
|
|
262
266
|
const debt = debtOf(st, d.sub)
|
|
267
|
+
const desde = d.addedAt ? t.muted(` ${i.deviceSince}:${fmtExp(d.addedAt)}`) : ''
|
|
263
268
|
const extra = (d.certCount > 1 ? t.muted(` certs:${d.certCount}`) : '') +
|
|
264
269
|
(vars ? t.muted(` vars:${vars}`) : '') +
|
|
265
270
|
// EN DEUDA: en el acta y sin poder abrir lo suyo. Va en color de aviso al lado de
|
|
@@ -272,7 +277,7 @@ function deviceRows (st, t) {
|
|
|
272
277
|
: d.isMaster
|
|
273
278
|
? t.muted(i.thisVault)
|
|
274
279
|
: t.muted('scope:' + shortScope(d.scope)) + ' ' + t.muted('exp:' + fmtExp(d.exp))
|
|
275
|
-
rows.push({ text: ` ${t.bold(d.deviceId)} ${label} ${status}${extra}`, sel: true, meta: d })
|
|
280
|
+
rows.push({ text: ` ${t.bold(d.deviceId)} ${label}${desde} ${status}${extra}`, sel: true, meta: d })
|
|
276
281
|
}
|
|
277
282
|
const revoked = st.devices?.revoked || []
|
|
278
283
|
if (revoked.length) {
|
|
@@ -318,7 +323,17 @@ function pairModeRows (st, t) {
|
|
|
318
323
|
* una marca de si los tiene. El de administrar va aparte y avisado: es el único que deja
|
|
319
324
|
* a ese aparato meter y sacar dispositivos sin venir aquí.
|
|
320
325
|
*/
|
|
321
|
-
|
|
326
|
+
/**
|
|
327
|
+
* TODOS los permisos que el acta reconoce para un aparato, en el orden en que se leen: de
|
|
328
|
+
* lo cotidiano a lo que cambia quién manda.
|
|
329
|
+
*
|
|
330
|
+
* `approve` y `sealer` faltaban, y solo se podían conceder por la CLI. Un permiso que
|
|
331
|
+
* existe y no se ve en la pantalla que se llama «permisos» hace creer que no existe — y
|
|
332
|
+
* `sealer` es justo el que hay que entender para el multivault. Se ven siempre, aunque en
|
|
333
|
+
* un teléfono normal `sealer` no lo vaya a usar nadie: verlo apagado explica el modelo
|
|
334
|
+
* (dueño, 2026-08-31).
|
|
335
|
+
*/
|
|
336
|
+
const CAPS_ORDER = ['sign', 'store', 'read', 'admin', 'approve', 'passwords', 'sealer']
|
|
322
337
|
|
|
323
338
|
function capsRows (st, t) {
|
|
324
339
|
const i = L(st)
|
|
@@ -918,12 +933,41 @@ async function descartarCuenta (term, st, id, volverA) {
|
|
|
918
933
|
if (r.ok) flash(st, i.accountDiscarded)
|
|
919
934
|
}
|
|
920
935
|
|
|
936
|
+
/**
|
|
937
|
+
* PRIMERO EL NOMBRE, y luego el QR.
|
|
938
|
+
*
|
|
939
|
+
* Sin preguntarlo, el nombre lo pone el propio aparato al enrolarse, y por defecto usa el
|
|
940
|
+
* apodo del PERFIL: te quedaban tres dispositivos llamados igual que tú y ninguna forma de
|
|
941
|
+
* saber cuál era cuál sin renombrarlos después —cuando ya no te acordabas—. Se pregunta
|
|
942
|
+
* ANTES porque después ya estás mirando un QR con el teléfono en la mano.
|
|
943
|
+
*
|
|
944
|
+
* Enter en blanco sigue valiendo: deja que lo diga el aparato, que es el comportamiento de
|
|
945
|
+
* siempre para quien no quiera decidirlo.
|
|
946
|
+
*/
|
|
947
|
+
function beginPairingNamed (term, st, profile, service = null, after = null) {
|
|
948
|
+
const i = L(st)
|
|
949
|
+
setInput(st, {
|
|
950
|
+
label: i.nameDeviceLabel,
|
|
951
|
+
hint: i.nameDeviceHint,
|
|
952
|
+
onSubmit: async (nombre) => {
|
|
953
|
+
st.input = null
|
|
954
|
+
const ok = await beginPairing(term, st, profile, service, (nombre || '').trim().slice(0, 60) || null)
|
|
955
|
+
// `after` es para quien tenga que limpiar si el emparejamiento no llega a abrirse
|
|
956
|
+
// (la cuenta que nació para él). Va aquí porque con el nombre de por medio ya no se
|
|
957
|
+
// puede mirar el valor de retorno desde fuera.
|
|
958
|
+
if (after) await after(ok)
|
|
959
|
+
},
|
|
960
|
+
onCancel: () => { st.input = null }
|
|
961
|
+
})
|
|
962
|
+
return true
|
|
963
|
+
}
|
|
964
|
+
|
|
921
965
|
/**
|
|
922
966
|
* Abre el emparejamiento contra `profile` y salta a la pantalla del QR. Con `service`,
|
|
923
967
|
* el QR es el de un SERVICIO de ese namespace (cert limitado a sus variables).
|
|
924
968
|
*/
|
|
925
|
-
async function beginPairing (term, st, profile, service = null) {
|
|
926
|
-
const r = await guard(term, st, L(st).startingPairing, () => vc.startPairing({ profile, ...(service ? { service } : {}) }))
|
|
969
|
+
async function beginPairing (term, st, profile, service = null, label = null) {
|
|
970
|
+
const r = await guard(term, st, L(st).startingPairing, () => vc.startPairing({ profile, ...(service ? { service } : {}), ...(label ? { label } : {}) }))
|
|
927
971
|
// `service` se pega al estado porque el daemon no lo devuelve: la pantalla del QR
|
|
928
972
|
// tiene que poder decir qué se está entregando, que no es lo mismo un aparato tuyo
|
|
929
973
|
// que una máquina que solo va a leer la configuración del proxy.
|
|
@@ -983,7 +1027,7 @@ async function onKeyPairMode (term, st, key) {
|
|
|
983
1027
|
if (key.name === 'escape' || ch === 'b') { st.screen = 'devices'; return true }
|
|
984
1028
|
if (key.name !== 'enter' || !cur) return true
|
|
985
1029
|
|
|
986
|
-
if (cur.mode === 'here') {
|
|
1030
|
+
if (cur.mode === 'here') { return beginPairingNamed(term, st, activeId(st)) }
|
|
987
1031
|
|
|
988
1032
|
// SERVICIO: entra a la cuenta activa, pero con un certificado que solo sirve para
|
|
989
1033
|
// pedir las variables de SU namespace. El nombre del ns es el que luego pide el
|
|
@@ -1025,8 +1069,10 @@ async function onKeyPairMode (term, st, key) {
|
|
|
1025
1069
|
// Si el emparejamiento no llega a abrirse, la cuenta que se creó PARA él se va con
|
|
1026
1070
|
// él: si no, cada intento fallido dejaba una cuenta vacía —y encima activa— que
|
|
1027
1071
|
// luego había que ir a borrar a mano adivinando cuál era.
|
|
1028
|
-
|
|
1029
|
-
|
|
1072
|
+
beginPairingNamed(term, st, created, null, async (ok) => {
|
|
1073
|
+
if (!ok) await descartarCuenta(term, st, created, previa)
|
|
1074
|
+
else st.pairing.born = { id: created, from: previa }
|
|
1075
|
+
})
|
|
1030
1076
|
},
|
|
1031
1077
|
onCancel: () => { st.input = null }
|
|
1032
1078
|
})
|
package/src/tui/i18n.js
CHANGED
|
@@ -46,14 +46,22 @@ const es = {
|
|
|
46
46
|
store: 'Guardar tus datos',
|
|
47
47
|
read: 'Leer tus datos',
|
|
48
48
|
admin: 'Administrar el perfil',
|
|
49
|
-
passwords: 'Pedir tus contraseñas'
|
|
49
|
+
passwords: 'Pedir tus contraseñas',
|
|
50
|
+
approve: 'Aprobar pedidos',
|
|
51
|
+
sealer: 'Sellar el acta'
|
|
50
52
|
},
|
|
51
53
|
capHint: {
|
|
52
54
|
sign: 'usar tu identidad en las apps del ecosistema',
|
|
53
55
|
store: 'escribir en tu bóveda (perfil, contenido, datos sensibles)',
|
|
54
56
|
read: 'ver lo que guardaste',
|
|
55
57
|
admin: 'conectar y quitar dispositivos desde ese aparato, sin venir aquí',
|
|
56
|
-
passwords: 'el gestor: pedir una credencial por sitio, nunca la lista entera'
|
|
58
|
+
passwords: 'el gestor: pedir una credencial por sitio, nunca la lista entera',
|
|
59
|
+
approve: 'dar el visto bueno cuando otro aparato pide claves privadas',
|
|
60
|
+
// Sellar solo significa algo en un aparato que HAGA de bóveda (otra máquina con el
|
|
61
|
+
// daemon, o un navegador que se puso de bóveda). En un teléfono normal se concede y no
|
|
62
|
+
// lo usa nadie. Se enseña igual, y se dice: verlo desactivado explica el modelo;
|
|
63
|
+
// esconderlo hacía creer que no existe (dueño, 2026-08-31).
|
|
64
|
+
sealer: 'admitir aparatos y cambiar permisos él solo, si esta bóveda no está. Solo sirve en otra BÓVEDA'
|
|
57
65
|
},
|
|
58
66
|
confirmAdmin: (id) => `¿Dejar que ${id} conecte y quite dispositivos sin venir aquí?`,
|
|
59
67
|
capGiven: (n) => `Concedido: ${n}`,
|
|
@@ -140,6 +148,9 @@ const es = {
|
|
|
140
148
|
cantRemoveMaster: 'Esta bóveda es la que manda: no se quita a sí misma.',
|
|
141
149
|
revokedCount: (n) => ` Revocados: ${n}`,
|
|
142
150
|
startingPairing: 'Iniciando emparejamiento…',
|
|
151
|
+
deviceSince: 'desde',
|
|
152
|
+
nameDeviceLabel: '¿Cómo se va a llamar este aparato?',
|
|
153
|
+
nameDeviceHint: 'Enter para dejar que lo diga él (usará tu apodo, y así no se distinguen)',
|
|
143
154
|
noPending: 'No hay ningún dispositivo pendiente',
|
|
144
155
|
noPendingToReject: 'No hay ningún dispositivo pendiente para rechazar',
|
|
145
156
|
rejecting: 'Rechazando…',
|
|
@@ -345,14 +356,18 @@ const en = {
|
|
|
345
356
|
store: 'Save your data',
|
|
346
357
|
read: 'Read your data',
|
|
347
358
|
admin: 'Manage the profile',
|
|
348
|
-
passwords: 'Ask for your passwords'
|
|
359
|
+
passwords: 'Ask for your passwords',
|
|
360
|
+
approve: 'Approve requests',
|
|
361
|
+
sealer: 'Seal the record'
|
|
349
362
|
},
|
|
350
363
|
capHint: {
|
|
351
364
|
sign: 'use your identity across the ecosystem apps',
|
|
352
365
|
store: 'write to your vault (profile, content, sensitive data)',
|
|
353
366
|
read: 'see what you saved',
|
|
354
367
|
admin: 'connect and remove devices from that device, without coming here',
|
|
355
|
-
passwords: 'the manager: ask for one credential per site, never the whole list'
|
|
368
|
+
passwords: 'the manager: ask for one credential per site, never the whole list',
|
|
369
|
+
approve: 'give the go-ahead when another device asks for private keys',
|
|
370
|
+
sealer: 'admit devices and change permissions on its own if this vault is gone. Only useful on another VAULT'
|
|
356
371
|
},
|
|
357
372
|
confirmAdmin: (id) => `Let ${id} connect and remove devices without coming here?`,
|
|
358
373
|
capGiven: (n) => `Granted: ${n}`,
|
|
@@ -435,6 +450,9 @@ const en = {
|
|
|
435
450
|
cantRemoveMaster: 'This vault is the Master: it does not remove itself.',
|
|
436
451
|
revokedCount: (n) => ` Revoked: ${n}`,
|
|
437
452
|
startingPairing: 'Starting pairing…',
|
|
453
|
+
deviceSince: 'since',
|
|
454
|
+
nameDeviceLabel: 'What will this device be called?',
|
|
455
|
+
nameDeviceHint: 'Enter to let it say (it will use your nickname, and then they all look alike)',
|
|
438
456
|
noPending: 'No device is waiting',
|
|
439
457
|
noPendingToReject: 'No device is waiting to be rejected',
|
|
440
458
|
rejecting: 'Rejecting…',
|
package/src/vaultControl.js
CHANGED
|
@@ -604,10 +604,10 @@ export function pairUrl (qr) {
|
|
|
604
604
|
return { url: inviteUrl(qr), code, payload: JSON.stringify(qr), b64: code }
|
|
605
605
|
}
|
|
606
606
|
|
|
607
|
-
export async function startPairing ({ profile, service } = {}) {
|
|
607
|
+
export async function startPairing ({ profile, service, label } = {}) {
|
|
608
608
|
requireAlive()
|
|
609
609
|
rm(F.pair); rm(F.pending)
|
|
610
|
-
await writeReq(F.pairReq, service ? { service } : {}, profile)
|
|
610
|
+
await writeReq(F.pairReq, { ...(service ? { service } : {}), ...(label ? { label } : {}) }, profile)
|
|
611
611
|
signalOrCleanup('SIGUSR1', [F.pairReq])
|
|
612
612
|
for (let i = 0; i < 50; i++) {
|
|
613
613
|
await sleep(100)
|