@dotrino/vaultd 0.150.1 → 0.152.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 +12 -7
- package/lib/src/service.js +3 -2
- package/package.json +2 -2
- package/src/ctl.js +8 -6
- package/src/daemon.js +1 -1
- package/src/tui/app.js +16 -19
- package/src/tui/i18n.js +2 -4
- package/src/vault.js +15 -10
package/lib/src/enroll.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* `docs/pairing-protocol.md`).
|
|
31
31
|
*/
|
|
32
32
|
import { verifyDeviceSig, pubkeyId, commitCode } from '@dotrino/identity/capabilities'
|
|
33
|
-
import { verifyContinuity, canSeal, DEVICE_CAPS } from '@dotrino/identity/acta'
|
|
33
|
+
import { verifyContinuity, canSeal, DEVICE_CAPS, VAULT_CN } from '@dotrino/identity/acta'
|
|
34
34
|
|
|
35
35
|
/** Un token de emparejamiento vale 5 min. */
|
|
36
36
|
export const PAIRING_TTL_MS = 5 * 60 * 1000
|
|
@@ -169,12 +169,16 @@ export function createEnrollDesk ({
|
|
|
169
169
|
* el código, igual que hacía `caps` después.
|
|
170
170
|
*/
|
|
171
171
|
async function startPairing ({ scope = defaultScope, ttlMs = defaultTtlMs, label = '', mode = 'join', account = '', caps = null } = {}) {
|
|
172
|
+
// El nombre de la propia bóveda no es un servicio: su llave firma con el perfil cerrado
|
|
173
|
+
// y no puede abrir ningún cajón. Un servicio que entrara con ese nombre tampoco podría.
|
|
174
|
+
if (scopeToCn(scope) === VAULT_CN) throw Object.assign(new Error(`"${VAULT_CN}" is the vault's own name: a service cannot use it`), { code: 'reserved-cn' })
|
|
172
175
|
if (caps != null) {
|
|
173
176
|
if (!Array.isArray(caps)) throw Object.assign(new Error('caps must be a list'), { code: 'bad-caps' })
|
|
174
177
|
const cn = scopeToCn(scope)
|
|
175
|
-
const bad = caps.filter((c) => !
|
|
178
|
+
const bad = caps.filter((c) => !DEVICE_CAPS.includes(c))
|
|
176
179
|
if (bad.length) throw Object.assign(new Error('unknown permission(s): ' + bad.join(', ')), { code: 'bad-caps' })
|
|
177
|
-
|
|
180
|
+
// Un servicio puede entrar sin ningún permiso: su cajón lo abre el CN.
|
|
181
|
+
if (!caps.length && !cn) throw Object.assign(new Error('the pairing grants no permission'), { code: 'empty-scope' })
|
|
178
182
|
caps = [...new Set(caps)]
|
|
179
183
|
}
|
|
180
184
|
pending.clear() // uno a la vez: una sesión nueva supersede a la anterior
|
|
@@ -378,13 +382,14 @@ export function createEnrollDesk ({
|
|
|
378
382
|
throw Object.assign(new Error('this vault cannot add devices to the account record: no certificate was issued'), { code: 'admit-unavailable' })
|
|
379
383
|
}
|
|
380
384
|
// PERMISOS, no tipos (2026-08-22): las capacidades son las del scope ENTERO. Un
|
|
381
|
-
// cajón (`secrets:<ns>`)
|
|
382
|
-
// con `sign,secrets:eco` firma como
|
|
385
|
+
// cajón (`secrets:<ns>`) fija el CN, que es lo que lo abre —no hay permiso aparte
|
|
386
|
+
// (dueño, 2026-10-13)—, y no borra lo demás: un bot con `sign,secrets:eco` firma como
|
|
387
|
+
// aparato del acta Y lee solo su cajón.
|
|
383
388
|
// Si quien emparejó ELIGIÓ los permisos (`startPairing({ caps })`), entran esos y en
|
|
384
389
|
// esta misma acta; si no, los que da el scope.
|
|
385
390
|
const cn = scopeToCn(pend.scope)
|
|
386
|
-
const caps = [...new Set(
|
|
387
|
-
if (!caps.length) {
|
|
391
|
+
const caps = [...new Set(pend.caps || scopeToCaps(pend.scope))]
|
|
392
|
+
if (!caps.length && !cn) {
|
|
388
393
|
throw Object.assign(new Error('the pairing scope grants no permission: no certificate was issued'), { code: 'empty-scope' })
|
|
389
394
|
}
|
|
390
395
|
|
package/lib/src/service.js
CHANGED
|
@@ -522,8 +522,9 @@ export async function registerEncKey ({ dir, ns, proxyUrl, masterPubkey, device,
|
|
|
522
522
|
* privadas al pasar.
|
|
523
523
|
*
|
|
524
524
|
* Lo que decide la bóveda, no este cliente:
|
|
525
|
-
* ·
|
|
526
|
-
* ·
|
|
525
|
+
* · AÑADIR una variable nueva es directo: se guarda en el acto;
|
|
526
|
+
* · EDITAR una que ya existe pide aprobación (la tanda entera queda en espera);
|
|
527
|
+
* · si nadie puede aprobar, una que ya exista falla con `exists`;
|
|
527
528
|
* · BORRAR no existe por aquí.
|
|
528
529
|
*
|
|
529
530
|
* @param {object} o
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.152.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": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@dotrino/compat": "0.4.0",
|
|
25
|
-
"@dotrino/identity": "^0.
|
|
25
|
+
"@dotrino/identity": "^0.111.0",
|
|
26
26
|
"@dotrino/opaque": "0.1.0",
|
|
27
27
|
"@dotrino/passmanager": "0.17.0",
|
|
28
28
|
"@dotrino/proxy-client": "0.30.1",
|
package/src/ctl.js
CHANGED
|
@@ -39,6 +39,7 @@ import { atRestFor, readConfig as readKekConfig, probe as probeKek, rekeyDir, en
|
|
|
39
39
|
import { VERSION } from './version.js'
|
|
40
40
|
import * as hw from './hwkeys.js'
|
|
41
41
|
import { isNewer } from './update.js'
|
|
42
|
+
import { COMM_CN } from './commKey.js'
|
|
42
43
|
|
|
43
44
|
const dir = dataDir()
|
|
44
45
|
const stateFile = path.join(dir, 'state.json')
|
|
@@ -614,7 +615,7 @@ async function cmdMembers () {
|
|
|
614
615
|
// español. Se resaltan como `admin` y `approve`: los cuatro cambian lo que ese aparato
|
|
615
616
|
// puede hacerle a la cuenta, y eso se lee de un vistazo o no se lee.
|
|
616
617
|
const CAP = {
|
|
617
|
-
sign: 'firma', store: 'guarda', read: 'lee',
|
|
618
|
+
sign: 'firma', store: 'guarda', read: 'lee',
|
|
618
619
|
admin: `${B}administra el perfil${Z}`, approve: `${B}aprueba pedidos${Z}`,
|
|
619
620
|
sealer: `${B}sella el acta${Z}`, passwords: `${B}pide contraseñas${Z}`,
|
|
620
621
|
// Resaltado como los otros cuatro: dice si esa máquina se lleva tus claves privadas
|
|
@@ -644,7 +645,8 @@ async function cmdMembers () {
|
|
|
644
645
|
m.isMe ? 'este dispositivo' : null,
|
|
645
646
|
m.cn ? `servicio «${m.cn}»` : null
|
|
646
647
|
].filter(Boolean)
|
|
647
|
-
|
|
648
|
+
// Un servicio sin permisos no está «sin nada»: su cajón lo abre el nombre, no un permiso.
|
|
649
|
+
const caps = m.caps.length ? m.caps.map((c) => CAP[c] || c).join(', ') : (m.cn && m.cn !== COMM_CN ? '(solo su cajón)' : '(sin permisos)')
|
|
648
650
|
// CUÁNDO ENTRÓ. El nombre lo pone el propio aparato al emparejarse y muchas veces no
|
|
649
651
|
// distingue nada —dos teléfonos con el mismo apodo, o «cli» a secas—, así que la fecha
|
|
650
652
|
// es lo que deja reconocer cuál es cuál y ver si hay uno que no recuerdas haber
|
|
@@ -656,13 +658,13 @@ async function cmdMembers () {
|
|
|
656
658
|
// selladas a esa llave. Se dice aquí, junto a él, porque es el único sitio donde
|
|
657
659
|
// se mira quién es quién — y en la lista de variables ya seria tarde.
|
|
658
660
|
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
//
|
|
661
|
+
// Menos la llave de comunicación de la bóveda: entra con `cn: 'vault'` y solo `firma`,
|
|
662
|
+
// y no tiene llave de cifrado a propósito: salía marcada en rojo «NO puede leer sus
|
|
663
|
+
// variables» cuando no tiene ninguna que leer.
|
|
662
664
|
// SI LE LLEGAN LOS AVISOS, en quien aprueba. No cambia a quién se le pide —se le pide a
|
|
663
665
|
// todos—: es para ver a cuál no le suena el pedido.
|
|
664
666
|
if (m.caps.includes('approve') && 'notifiedAt' in m) console.log(' %s%s%s', D, avisosTxt(m.notifiedAt), Z)
|
|
665
|
-
if (m.cn &&
|
|
667
|
+
if (m.cn && m.cn !== COMM_CN && !m.canSeal) console.log(' %ssin llave de cifrado: NO puede leer sus variables%s', R, Z)
|
|
666
668
|
// QUÉ CORRE, y si cuadra con esta bóveda (CONVENCIONES §14). Sin esto una
|
|
667
669
|
// incompatibilidad de versiones se ve como que ese aparato «no responde», que es el
|
|
668
670
|
// fallo más caro que ha tenido el ecosistema. No bloquea nada: se enseña.
|
package/src/daemon.js
CHANGED
|
@@ -245,7 +245,7 @@ export async function runDaemon () {
|
|
|
245
245
|
if (caps) {
|
|
246
246
|
caps = [...new Set(caps)]
|
|
247
247
|
const cn = scopeToCn(base)
|
|
248
|
-
const fromCaps = caps.map((c) => capScope(c
|
|
248
|
+
const fromCaps = caps.map((c) => capScope(c)).filter((x) => x && ALLOWED(x))
|
|
249
249
|
// Sin `--scope` el papel sale de los permisos; con él, se suman.
|
|
250
250
|
scope = [...new Set([...(asked?.length || isService ? base : []), ...fromCaps])]
|
|
251
251
|
if (!scope.length) {
|
package/src/tui/app.js
CHANGED
|
@@ -562,17 +562,12 @@ const CAPS_ORDER = [...ORDEN.filter((c) => DEVICE_CAPS.includes(c)),
|
|
|
562
562
|
...DEVICE_CAPS.filter((c) => !ORDEN.includes(c))]
|
|
563
563
|
|
|
564
564
|
/**
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
* igual que pasaba con `unattended`. Se enseña primero, que es su importancia real.
|
|
571
|
-
*
|
|
572
|
-
* A un aparato SIN `cn` no se le ofrece: no hay cajón que abrir, el acta lo filtra
|
|
573
|
-
* (`allowedCaps`) y enseñar una casilla que no hace nada es peor que no enseñarla.
|
|
565
|
+
* EL CAJÓN NO ES UNA CASILLA (dueño, 2026-10-13: «no entiendo por qué es un permiso el
|
|
566
|
+
* acceso a un cajón, el cn ya garantiza eso»). Antes un servicio traía aquí «secretos
|
|
567
|
+
* «passmanager»» al lado de «contrasenas», y parecían dos permisos sobre lo mismo. Un
|
|
568
|
+
* servicio abre el cajón de su nombre por ser ese servicio: se DICE arriba de la lista
|
|
569
|
+
* (`capsDrawer`), y no se marca ni se desmarca.
|
|
574
570
|
*/
|
|
575
|
-
const capsForMember = (member) => (member?.cn ? ['secrets', ...CAPS_ORDER] : CAPS_ORDER)
|
|
576
571
|
|
|
577
572
|
/** Con lo que entra un aparato si no eliges otra cosa: lo mismo que `pair` sin `--scope`. */
|
|
578
573
|
const PAIR_DEFAULT_CAPS = ['sign', 'read', 'store']
|
|
@@ -586,11 +581,10 @@ const PAIR_DEFAULT_CAPS = ['sign', 'read', 'store']
|
|
|
586
581
|
function capChecklist (i, t, { cn = null, has, real = has }) {
|
|
587
582
|
const rows = []
|
|
588
583
|
let tocados = 0
|
|
589
|
-
|
|
584
|
+
if (cn) rows.push({ text: t.muted(' ' + i.capsDrawer(cn)), sel: false }, { text: '', sel: false })
|
|
585
|
+
for (const cap of CAPS_ORDER) {
|
|
590
586
|
const mark = has.has(cap) ? '[x]' : '[ ]'
|
|
591
|
-
|
|
592
|
-
// «lee sus claves» te deja preguntándote cuáles.
|
|
593
|
-
const name = typeof i.capName[cap] === 'function' ? i.capName[cap](cn) : i.capName[cap]
|
|
587
|
+
const name = i.capName[cap]
|
|
594
588
|
// LO QUE CAMBIA SE SEÑALA. Sin esto el borrador es indistinguible del acta y no sabes
|
|
595
589
|
// qué vas a firmar — que es justo lo que hace peligroso acumular cambios.
|
|
596
590
|
const cambia = has.has(cap) !== real.has(cap)
|
|
@@ -599,8 +593,7 @@ function capChecklist (i, t, { cn = null, has, real = has }) {
|
|
|
599
593
|
// así que no hay que repetirlo al lado. Lo que hace va debajo.
|
|
600
594
|
const line = ` ${mark}${cambia ? t.bold('*') : ' '} ${cap === 'admin' ? t.bold(name) : name}`
|
|
601
595
|
rows.push({ text: line, sel: true, meta: { cap } })
|
|
602
|
-
|
|
603
|
-
rows.push({ text: t.muted(' ' + hint), sel: false })
|
|
596
|
+
rows.push({ text: t.muted(' ' + i.capHint[cap]), sel: false })
|
|
604
597
|
}
|
|
605
598
|
return { rows, tocados }
|
|
606
599
|
}
|
|
@@ -1344,7 +1337,8 @@ async function onKeyDevices (term, st, key) {
|
|
|
1344
1337
|
value: cur.label || '',
|
|
1345
1338
|
onSubmit: async (raw) => {
|
|
1346
1339
|
const name = String(raw || '').trim()
|
|
1347
|
-
|
|
1340
|
+
st.input = null
|
|
1341
|
+
if (!name) { flash(st, i.nameEmpty, 'danger'); return }
|
|
1348
1342
|
const r = await guard(term, st, i.renaming, () => vc.setDeviceLabel(cur.sub, name, activeId(st)))
|
|
1349
1343
|
if (r.ok) { applyDump(st, r.v); flash(st, i.deviceRenamed(name)) }
|
|
1350
1344
|
}
|
|
@@ -1415,6 +1409,7 @@ async function onKeyLogins (term, st, key, api = vc) {
|
|
|
1415
1409
|
mask: true,
|
|
1416
1410
|
onSubmit: async (pw2) => {
|
|
1417
1411
|
if (pw1 !== pw2) { flash(st, i.loginPassMismatch, 'warn'); return }
|
|
1412
|
+
st.input = null
|
|
1418
1413
|
const r = await guard(term, st, i.loginCreating,
|
|
1419
1414
|
() => api.addLogin({ user, password: pw1, label }))
|
|
1420
1415
|
if (r.ok) {
|
|
@@ -1442,6 +1437,7 @@ async function onKeyLogins (term, st, key, api = vc) {
|
|
|
1442
1437
|
mask: true,
|
|
1443
1438
|
onSubmit: async (nueva) => {
|
|
1444
1439
|
if (String(nueva || '').length < MIN_LOGIN_PASSWORD) { flash(st, i.loginPassShort, 'warn'); return }
|
|
1440
|
+
st.input = null
|
|
1445
1441
|
const r = await guard(term, st, i.loginChanging,
|
|
1446
1442
|
() => api.passwdLogin({ user: cur.user, oldPassword: vieja, newPassword: nueva }))
|
|
1447
1443
|
if (r.ok) { flash(st, i.loginChanged); await refreshLogins(term, st, api) }
|
|
@@ -1593,7 +1589,8 @@ async function onKeyPairCaps (term, st, key) {
|
|
|
1593
1589
|
return true
|
|
1594
1590
|
}
|
|
1595
1591
|
if (ch === 'g') {
|
|
1596
|
-
|
|
1592
|
+
// Un servicio puede entrar sin ningún permiso: su cajón lo abre el nombre.
|
|
1593
|
+
if (!pc.caps.length && !pc.service) { flash(st, i.pairCapsNone, 'warn'); return true }
|
|
1597
1594
|
const go = async () => {
|
|
1598
1595
|
st.pairCaps = null
|
|
1599
1596
|
const ok = await beginPairing(term, st, pc.profile, pc.service, pc.label, pc.caps)
|
|
@@ -1614,7 +1611,7 @@ async function onKeyPairCaps (term, st, key) {
|
|
|
1614
1611
|
|
|
1615
1612
|
/** Abre la pantalla de permisos del emparejamiento; el QR sale al pulsar G. */
|
|
1616
1613
|
function choosePairCaps (st, { profile, service = null, label = null, after = null }) {
|
|
1617
|
-
st.pairCaps = { profile, service, label, after, caps: service ? [
|
|
1614
|
+
st.pairCaps = { profile, service, label, after, caps: service ? [] : [...PAIR_DEFAULT_CAPS] }
|
|
1618
1615
|
st.sel.paircaps = 0
|
|
1619
1616
|
st.scroll.paircaps = { value: 0 }
|
|
1620
1617
|
st.screen = 'paircaps'
|
package/src/tui/i18n.js
CHANGED
|
@@ -79,6 +79,7 @@ const es = {
|
|
|
79
79
|
// PERMISOS de un dispositivo (§9.1: se dice el beneficio, no el scope del cert).
|
|
80
80
|
titleCaps: 'Permisos del dispositivo',
|
|
81
81
|
capsFor: (id, name) => `Permisos de ${id}${name ? ' · ' + name : ''}`,
|
|
82
|
+
capsDrawer: (cn) => `Es el servicio «${cn}»: abre el cajón «${cn}» y ningún otro.`,
|
|
82
83
|
capsNoMember: ' (este dispositivo ya no está en el perfil)',
|
|
83
84
|
capsApplyHint: 'Enter marca o desmarca · G guarda · Esc vuelve a la lista sin perder lo marcado',
|
|
84
85
|
capsPending: (n) => `${n} cambio(s) sin guardar en este aparato (*) · G guarda todo en una sola acta`,
|
|
@@ -104,7 +105,6 @@ const es = {
|
|
|
104
105
|
// resultado fue una lista donde no se sabía qué permiso era cuál ni cómo nombrarlo para
|
|
105
106
|
// el CLI. Lo que hace cada uno va DEBAJO, que es su sitio.
|
|
106
107
|
capName: {
|
|
107
|
-
secrets: (cn) => `secretos «${cn}»`,
|
|
108
108
|
sign: 'firma',
|
|
109
109
|
store: 'guarda',
|
|
110
110
|
read: 'lee',
|
|
@@ -120,7 +120,6 @@ const es = {
|
|
|
120
120
|
// continuaba a «Conectar y quitar dispositivos»—; ahora el título es la palabra que se
|
|
121
121
|
// teclea, así que la pista tiene que decir la frase entera.
|
|
122
122
|
capHint: {
|
|
123
|
-
secrets: (cn) => `abrir el cajón de «${cn}» y ningún otro`,
|
|
124
123
|
sign: 'usar tu identidad en las apps del ecosistema',
|
|
125
124
|
store: 'escribir en tu bóveda: perfil, contenido, datos',
|
|
126
125
|
read: 'ver lo que guardaste',
|
|
@@ -564,6 +563,7 @@ const en = {
|
|
|
564
563
|
tabMe: 'Profile',
|
|
565
564
|
titleCaps: 'Device permissions',
|
|
566
565
|
capsFor: (id, name) => `Permissions for ${id}${name ? ' · ' + name : ''}`,
|
|
566
|
+
capsDrawer: (cn) => `It is the “${cn}” service: it opens the “${cn}” drawer and no other.`,
|
|
567
567
|
capsNoMember: ' (this device is no longer in the record)',
|
|
568
568
|
capsApplyHint: 'Enter ticks or unticks · G saves · Esc goes back to the list keeping your ticks',
|
|
569
569
|
capsPending: (n) => `${n} unsaved change(s) on this device (*) · G saves everything in ONE record`,
|
|
@@ -583,7 +583,6 @@ const en = {
|
|
|
583
583
|
// El título es el NOMBRE del permiso, el mismo que se teclea. Ver la nota en el bloque
|
|
584
584
|
// español: describirlo dejaba una lista donde no se sabía qué permiso era cuál.
|
|
585
585
|
capName: {
|
|
586
|
-
secrets: (cn) => `secrets «${cn}»`,
|
|
587
586
|
sign: 'sign',
|
|
588
587
|
store: 'store',
|
|
589
588
|
read: 'read',
|
|
@@ -597,7 +596,6 @@ const en = {
|
|
|
597
596
|
},
|
|
598
597
|
// Cada pista se sostiene sola: el título es la palabra, no una frase que continuar.
|
|
599
598
|
capHint: {
|
|
600
|
-
secrets: (cn) => `open the «${cn}» drawer and no other`,
|
|
601
599
|
sign: 'use your identity across the ecosystem apps',
|
|
602
600
|
store: 'write to your vault: profile, content, data',
|
|
603
601
|
read: 'see what you saved',
|
package/src/vault.js
CHANGED
|
@@ -1477,9 +1477,9 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1477
1477
|
* firma del autor, las envolturas, la espera de aprobación. Aquí no hay otra puerta.
|
|
1478
1478
|
*
|
|
1479
1479
|
* Los límites (dueño, 2026-09-29):
|
|
1480
|
-
* ·
|
|
1481
|
-
*
|
|
1482
|
-
* · si nadie puede aprobar, no se pide y
|
|
1480
|
+
* · AÑADIR una variable nueva es directo, haya o no quien apruebe (dueño, 2026-10-12);
|
|
1481
|
+
* · EDITAR una que ya existe pide aprobación —`unattended` no exime—;
|
|
1482
|
+
* · si nadie puede aprobar, no se pide y lo que ya existe no se toca (`exists`);
|
|
1483
1483
|
* · BORRAR, nunca: eso es de la consola y del CLI de la bóveda.
|
|
1484
1484
|
*
|
|
1485
1485
|
* Dos operaciones: `store.recipients` (para quién se sella: solo públicas, nada que ocultar)
|
|
@@ -1696,7 +1696,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1696
1696
|
log(`[vault] ${ns}: ${deviceId || '????-????'} wants to store ${items.length} variable(s) — waiting for approval (${result.pending})`)
|
|
1697
1697
|
} else {
|
|
1698
1698
|
audit('store', { device: deviceId, ns, keys: result?.keys || [] })
|
|
1699
|
-
log(`[vault] ${ns}: ${deviceId || '????-????'} stored ${(result?.keys || []).length} variable(s)
|
|
1699
|
+
log(`[vault] ${ns}: ${deviceId || '????-????'} stored ${(result?.keys || []).length} new variable(s)`)
|
|
1700
1700
|
await notifyMembers('vars', { by: deviceId, keys: result?.keys || [], ns })
|
|
1701
1701
|
}
|
|
1702
1702
|
return await answer({ op: 'store.result', ns, ts: Date.now(), keys: result?.keys || [], pending: result?.pending || null })
|
|
@@ -2811,13 +2811,18 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2811
2811
|
const owner = ns ? `ns:${ns}` : `dev:${pub}`
|
|
2812
2812
|
const keys = []
|
|
2813
2813
|
// ¿SE PIDE APROBACIÓN? Desde la mesa del admin, lo de siempre (`needsApproval`: quien
|
|
2814
|
-
// tiene `unattended` escribe solo). Desde el propio SERVICIO que guarda en su cajón,
|
|
2815
|
-
//
|
|
2816
|
-
//
|
|
2817
|
-
//
|
|
2818
|
-
//
|
|
2814
|
+
// tiene `unattended` escribe solo). Desde el propio SERVICIO que guarda en su cajón, la
|
|
2815
|
+
// aprobación es SOLO PARA EDITAR (dueño, 2026-10-12: «la aprobación solamente es para
|
|
2816
|
+
// editar una env; para almacenarla es directo»): una variable nueva no le quita nada a
|
|
2817
|
+
// nadie y se guarda en el acto; reemplazar una que ya existe pide el sí de quien
|
|
2818
|
+
// aprueba, tenga o no `unattended` el servicio (dueño, 2026-09-29: «el unattended
|
|
2819
|
+
// también pediría aprobación»). Una tanda con alguna que ya existe se pide ENTERA: va
|
|
2820
|
+
// todo o nada. Sin nadie que apruebe no se pide —la regla de siempre— y lo que ya
|
|
2821
|
+
// existe falla con `exists`.
|
|
2819
2822
|
const record = await refreshActa()
|
|
2820
|
-
const ask = fromService
|
|
2823
|
+
const ask = fromService
|
|
2824
|
+
? hasApprover(record) && items.some((it) => it?.key && secrets.has(owner, it.key))
|
|
2825
|
+
: needsApproval(caller, record)
|
|
2821
2826
|
for (const it of items) {
|
|
2822
2827
|
if (!it?.key || !it?.sealed) throw new Error('var.setMany: each variable needs its key and its sealed envelope')
|
|
2823
2828
|
await checkAuthor(owner, it.key, it.sealed, caller, { allowOverwrite: ask })
|