@dotrino/vaultd 0.85.0 → 0.87.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/package.json +1 -1
- package/src/ctl.js +19 -11
- package/src/secretsStore.js +22 -6
- package/src/vault.js +44 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.87.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
|
@@ -960,7 +960,9 @@ function problemWith (key, value) {
|
|
|
960
960
|
}
|
|
961
961
|
|
|
962
962
|
async function cmdSecret (rest) {
|
|
963
|
-
// --public / --private: si
|
|
963
|
+
// --public / --private: si al entregarla se pide APROBACIÓN. NO cambia cómo se guarda —
|
|
964
|
+
// desde 2026-09-02 las dos van selladas igual (dueño: «la única diferencia es si se
|
|
965
|
+
// despachan o no, son políticas; dales el mismo tratamiento de seguridad»).
|
|
964
966
|
// Se sacan de la línea antes de partirla, para que puedan ir en cualquier posición y no
|
|
965
967
|
// se cuelen dentro del valor (que es lo último y puede llevar espacios).
|
|
966
968
|
let isPublic
|
|
@@ -1054,8 +1056,8 @@ async function cmdSecret (rest) {
|
|
|
1054
1056
|
'la bóveda la aplica entera y avisa UNA sola vez. De una en una, cada variable es un',
|
|
1055
1057
|
'cambio de configuración y el servicio se reinicia a media carga.',
|
|
1056
1058
|
'',
|
|
1057
|
-
'Pública o privada dice UNA cosa: si
|
|
1058
|
-
'
|
|
1059
|
+
'Pública o privada dice UNA cosa: si al entregarla se te pide APROBACIÓN. Las dos se',
|
|
1060
|
+
'guardan cifradas igual y ninguna enseña su valor en la consola. Se nace privada.'
|
|
1059
1061
|
].join('\n')
|
|
1060
1062
|
|
|
1061
1063
|
/**
|
|
@@ -1108,11 +1110,14 @@ async function cmdSecret (rest) {
|
|
|
1108
1110
|
}
|
|
1109
1111
|
|
|
1110
1112
|
/**
|
|
1111
|
-
* Una variable en la lista: su nombre y
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1113
|
+
* Una variable en la lista: su nombre, y que TIENE valor. Ninguna lo enseña.
|
|
1114
|
+
*
|
|
1115
|
+
* La pública lo enseñaba, porque se guardaba en claro. Desde 2026-09-02 las dos van
|
|
1116
|
+
* selladas igual y `pública` solo dice que se entrega sin pedir aprobación (dueño: «la
|
|
1117
|
+
* única diferencia es si se despachan o no, son políticas»). Enseñar el hueco vacío de
|
|
1118
|
+
* una pública era peor que no enseñar nada: parecía que se había perdido.
|
|
1114
1119
|
*/
|
|
1115
|
-
const printVar = (k) => console.log(' · %s
|
|
1120
|
+
const printVar = (k) => console.log(' · %s ••••••%s', k.key, k.public ? ' (pública)' : '')
|
|
1116
1121
|
/**
|
|
1117
1122
|
* Por qué no se aplicó. El daemon deja el motivo en el volcado siguiente; sin él lo
|
|
1118
1123
|
* único que se podía decir era «revisa los logs», que no ayuda a quien acaba de
|
|
@@ -1179,7 +1184,10 @@ async function cmdSecret (rest) {
|
|
|
1179
1184
|
console.log('%s%s%s%s', B, x.id, Z, who ? ' ' + who : '')
|
|
1180
1185
|
for (const k of x.keys) printVar(k)
|
|
1181
1186
|
}
|
|
1182
|
-
|
|
1187
|
+
// QUÉ SIGNIFICA HOY (dueño, 2026-09-02): todas se guardan selladas igual, y `pública`
|
|
1188
|
+
// solo dice a quién se le entrega SIN APROBACIÓN. Decir «su valor se puede ver» era
|
|
1189
|
+
// verdad cuando se guardaba en claro; dejarlo ahí sería prometer algo que ya no pasa.
|
|
1190
|
+
console.log('\n(pública) = se entrega sin pedirte aprobación. Todas se guardan cifradas igual.\n')
|
|
1183
1191
|
printPending(d)
|
|
1184
1192
|
await printNoPassword()
|
|
1185
1193
|
return
|
|
@@ -1534,9 +1542,9 @@ function help () {
|
|
|
1534
1542
|
vuelve a una versión anterior
|
|
1535
1543
|
secret settle salda lo pendiente: hereda lo ya guardado a un
|
|
1536
1544
|
aparato nuevo y rota de verdad el cajón del que se fue
|
|
1537
|
-
--public | --private (al hacer un set) si
|
|
1538
|
-
|
|
1539
|
-
y una privada NO se vuelve pública
|
|
1545
|
+
--public | --private (al hacer un set) si al entregarla se te pide
|
|
1546
|
+
APROBACIÓN. Las dos se guardan cifradas igual. Se nace
|
|
1547
|
+
privada, y una privada NO se vuelve pública.
|
|
1540
1548
|
secret visibility <ns> <CLAVE> private tapa una pública sin tocar el valor
|
|
1541
1549
|
secret device visibility <ID> <CLAVE> private
|
|
1542
1550
|
join <invitación> [--name <n>] [--kms <config.json>]
|
package/src/secretsStore.js
CHANGED
|
@@ -296,8 +296,13 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
/** Los destinatarios de un cajón: los que dice el acta, más la copia de recuperación. */
|
|
299
|
-
|
|
300
|
-
|
|
299
|
+
/**
|
|
300
|
+
* @param {boolean} isPublic Una PÚBLICA se envuelve además para quien administra, aunque
|
|
301
|
+
* el cajón tenga dueño (dueño, 2026-09-02). Puede variar por variable porque cada
|
|
302
|
+
* escritura estrena su propia CEK: el llavero guarda una generación por valor.
|
|
303
|
+
*/
|
|
304
|
+
const wrapAll = async (cek, owner, isPublic = false) => {
|
|
305
|
+
const members = (await recipients?.(owner, { public: isPublic })) || []
|
|
301
306
|
const { wraps, sinLlave } = await sealer.wrapFor(cek, members)
|
|
302
307
|
wraps[RECOVERY] = await sealer.wrapForKey(cek, data.recovery.pub)
|
|
303
308
|
return { wraps, sinLlave }
|
|
@@ -537,7 +542,7 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
|
|
|
537
542
|
// CEK NUEVA, siempre: no se puede reutilizar la de antes sin poder abrirla.
|
|
538
543
|
const cek = await sealer.newKey()
|
|
539
544
|
const gen = (topGen(bag, k)?.gen || 0) + 1
|
|
540
|
-
const { wraps, sinLlave } = await wrapAll(cek, owner)
|
|
545
|
+
const { wraps, sinLlave } = await wrapAll(cek, owner, pub)
|
|
541
546
|
const e = await sealer.encrypt(cek, value, gen)
|
|
542
547
|
// La FIRMA dice que este sobre salió de esta bóveda, y con qué acta (§8.8). Si no
|
|
543
548
|
// hay con qué firmar, el sobre sale sin firma: guardar es más importante.
|
|
@@ -631,9 +636,9 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
|
|
|
631
636
|
* haya dos sitios respondiendo a «quién puede abrir este cajón» (la lista la sabe el
|
|
632
637
|
* acta, y de ahí sale `recipients`).
|
|
633
638
|
*/
|
|
634
|
-
async recipientsFor (owner) {
|
|
639
|
+
async recipientsFor (owner, { public: isPublic = false } = {}) {
|
|
635
640
|
await ensureRecovery(null)
|
|
636
|
-
const members = (await recipients?.(owner)) || []
|
|
641
|
+
const members = (await recipients?.(owner, { public: isPublic })) || []
|
|
637
642
|
return {
|
|
638
643
|
recoveryPub: data.recovery.pub,
|
|
639
644
|
members: members.filter((m) => m.encPub).map((m) => ({ pub: m.pub, encPub: m.encPub }))
|
|
@@ -796,6 +801,13 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
|
|
|
796
801
|
return [...Object.keys(data.ns).map((k) => `ns:${k}`), ...Object.keys(data.dev).map((k) => `dev:${k}`)]
|
|
797
802
|
},
|
|
798
803
|
/** @private */
|
|
804
|
+
/**
|
|
805
|
+
* @param {Array|((isPublic:boolean)=>Promise<Array>)} members A quién envolver. Puede ser
|
|
806
|
+
* una FUNCIÓN de la visibilidad: desde 2026-09-02 una pública se envuelve además para
|
|
807
|
+
* quien administra, y eso se decide por variable — cada escritura estrena su CEK, así
|
|
808
|
+
* que el llavero tiene una generación por valor. Con una lista fija se aplicaría la
|
|
809
|
+
* misma a todas y se le daría a la consola la envoltura de una privada con dueño.
|
|
810
|
+
*/
|
|
799
811
|
async _rewrap (owner, members, adminKey = null, { exact = false } = {}) {
|
|
800
812
|
needSealer('re-wrap the key of a drawer')
|
|
801
813
|
if (needsMigration()) throw new NeedsMigration()
|
|
@@ -805,11 +817,15 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
|
|
|
805
817
|
const priv = await openRecovery(adminKey)
|
|
806
818
|
let wrapped = 0
|
|
807
819
|
const sinLlave = new Set()
|
|
820
|
+
// De qué variable es cada generación: es lo que dice si esa CEK es de una pública.
|
|
821
|
+
const visibilidad = new Map()
|
|
822
|
+
for (const e of Object.values(varsOf(bag, k))) if (e.gen != null) visibilidad.set(e.gen, !!e.pub)
|
|
823
|
+
const paraGen = async (gen) => (typeof members === 'function' ? members(visibilidad.get(gen) === true) : members)
|
|
808
824
|
for (const g of bag[k].keyring || []) {
|
|
809
825
|
const w = g.wraps?.[RECOVERY]
|
|
810
826
|
if (!w) continue
|
|
811
827
|
const cek = await sealer.openWrapWith(priv, w)
|
|
812
|
-
const r = await sealer.wrapFor(cek,
|
|
828
|
+
const r = await sealer.wrapFor(cek, await paraGen(g.gen))
|
|
813
829
|
// `exact`: el llavero queda con lo que dice el acta y NADA más. Es lo que hace
|
|
814
830
|
// falta para rehacerlo al abrir la bóveda:
|
|
815
831
|
// · una envoltura basura que metió alguien se reemplaza por la buena;
|
package/src/vault.js
CHANGED
|
@@ -197,7 +197,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
197
197
|
// A QUIÉN se le envuelve la llave de cada cajón: los servicios de ese namespace (o el
|
|
198
198
|
// propio aparato, si el cajón es suyo) MÁS los aparatos que administran. Sale del
|
|
199
199
|
// acta, y por eso lo pone el vault: el store no conoce el acta.
|
|
200
|
-
recipients: (owner) => recipientsOf(owner),
|
|
200
|
+
recipients: (owner, opts) => recipientsOf(owner, opts),
|
|
201
201
|
// La FIRMA del sobre: dice que salió de esta bóveda y con qué acta (§8.8).
|
|
202
202
|
signer: (body) => signSeal(body),
|
|
203
203
|
// CON QUÉ se cierra la copia de recuperación cuando quien llama no trajo la frase:
|
|
@@ -808,7 +808,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
808
808
|
// Y su cajón PROPIO también: un aparato con variables suyas se quedaba sin poder
|
|
809
809
|
// abrirlas, que es el mismo agujero por otra puerta.
|
|
810
810
|
for (const owner of [`ns:${ns}`, `dev:${chk.device}`]) {
|
|
811
|
-
await spreadKey(owner,
|
|
811
|
+
await spreadKey(owner, (esPublica) => recipientsOf(owner, { public: esPublica }))
|
|
812
812
|
.catch((e) => log(`[vault] ${owner}: could not hand it the key: ${e.message}`))
|
|
813
813
|
}
|
|
814
814
|
const body = { op: 'secrets.result', ns, enc: null, ok: true, ts: Date.now() }
|
|
@@ -1386,8 +1386,12 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1386
1386
|
* (`recipientsOf`), y tener dos sitios respondiendo a «quién puede abrir este cajón» es
|
|
1387
1387
|
* exactamente cómo se acaba dejando fuera a alguien sin que nadie se entere.
|
|
1388
1388
|
*/
|
|
1389
|
-
async recipients ({ ns, pub }) {
|
|
1390
|
-
|
|
1389
|
+
async recipients ({ ns, pub, public: isPublic }) {
|
|
1390
|
+
// LA VISIBILIDAD CAMBIA LA LISTA: una pública se envuelve además para quien
|
|
1391
|
+
// administra, así que hay que decirlo al preguntar. Pedirla sin decirlo y luego
|
|
1392
|
+
// guardarla como pública produciría un sobre que la consola no puede abrir — y la
|
|
1393
|
+
// bóveda lo rechazaría, que es lo correcto pero desorienta.
|
|
1394
|
+
return secrets.recipientsFor(ns ? `ns:${ns}` : `dev:${pub}`, { public: !!isPublic })
|
|
1391
1395
|
},
|
|
1392
1396
|
/**
|
|
1393
1397
|
* GUARDAR UNA VARIABLE **SIN VERLA** (dueño, 2026-09-01).
|
|
@@ -1416,7 +1420,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1416
1420
|
// despachan o no, son políticas; dales el mismo tratamiento de seguridad»). La
|
|
1417
1421
|
// marca solo decide si se entrega sin aprobación.
|
|
1418
1422
|
await verificarAutor(owner, key, sealed, caller)
|
|
1419
|
-
await verificarDestinatarios(owner, sealed.wraps)
|
|
1423
|
+
await verificarDestinatarios(owner, sealed.wraps, !!isPublic)
|
|
1420
1424
|
await secrets.putSealed(owner, key, sealed, { by: who, public: isPublic })
|
|
1421
1425
|
audit('secret.set', { ns: ns || null, key, sealed: true }); scheduleNotice(ns)
|
|
1422
1426
|
await settleDebts(owner)
|
|
@@ -1456,7 +1460,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1456
1460
|
for (const it of items) {
|
|
1457
1461
|
if (!it?.key || !it?.sealed) throw new Error('var.setMany: each variable needs its key and its sealed envelope')
|
|
1458
1462
|
await verificarAutor(owner, it.key, it.sealed, caller)
|
|
1459
|
-
await verificarDestinatarios(owner, it.sealed.wraps)
|
|
1463
|
+
await verificarDestinatarios(owner, it.sealed.wraps, !!it.public)
|
|
1460
1464
|
}
|
|
1461
1465
|
// Se comprueban TODAS antes de escribir NINGUNA: media carga aplicada es una
|
|
1462
1466
|
// configuración que nadie quiso, y el servicio se reinicia con ella.
|
|
@@ -1660,24 +1664,43 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1660
1664
|
}
|
|
1661
1665
|
|
|
1662
1666
|
/** Sin duplicar: un cosellador puede ser además el dueño del cajón. */
|
|
1667
|
+
/** Sin repetidos por llave: un aparato puede entrar por dos vías (dueño y admin). */
|
|
1668
|
+
const dedup = (miembros) => {
|
|
1669
|
+
const vistos = new Set()
|
|
1670
|
+
return miembros.filter((m) => m && !vistos.has(m.pub) && vistos.add(m.pub))
|
|
1671
|
+
}
|
|
1663
1672
|
const conCoselladores = (base, co) => {
|
|
1664
1673
|
const vistos = new Set(base.map((m) => m.pub))
|
|
1665
1674
|
return [...base, ...co.filter((m) => !vistos.has(m.pub))]
|
|
1666
1675
|
}
|
|
1667
1676
|
|
|
1668
|
-
async function recipientsOf (owner) {
|
|
1677
|
+
async function recipientsOf (owner, { public: isPublic = false } = {}) {
|
|
1669
1678
|
const co = await cosealerMembers()
|
|
1679
|
+
// UNA PÚBLICA SE ENVUELVE TAMBIÉN PARA QUIEN ADMINISTRA (dueño, 2026-09-02: «la consola
|
|
1680
|
+
// debe poder leer todas las variables… debe tener todos los sobres; la diferencia es si
|
|
1681
|
+
// el vault se los envía para la lectura o no» → «cambia la regla y di que se envuelven
|
|
1682
|
+
// las públicas»).
|
|
1683
|
+
//
|
|
1684
|
+
// Puede decidirse variable a variable porque cada escritura estrena su propia CEK: el
|
|
1685
|
+
// llavero guarda una generación por valor, no una por cajón.
|
|
1686
|
+
//
|
|
1687
|
+
// Y la PRIVADA de un cajón con dueño sigue sin envoltura para la consola, que es la
|
|
1688
|
+
// regla de agosto y su motivo no ha cambiado: el token de R2 no tiene por qué poder
|
|
1689
|
+
// abrirse desde un navegador. Lo que cambia es que «pública» ya sí quiere decir algo
|
|
1690
|
+
// para quien administra — puede leerla— además de que se despacha sin aprobación.
|
|
1691
|
+
const admins = isPublic ? await adminDevices() : []
|
|
1670
1692
|
if (owner.startsWith('ns:')) {
|
|
1671
1693
|
const owned = await nsMembers(owner.slice(3))
|
|
1672
|
-
// Sin dueño (un cajón personal, o uno cuyo servicio ya no está)
|
|
1673
|
-
//
|
|
1674
|
-
|
|
1694
|
+
// Sin dueño (un cajón personal, o uno cuyo servicio ya no está) entra quien administra
|
|
1695
|
+
// SIEMPRE: si no, no quedaría nadie que pudiera abrirlo sin la frase.
|
|
1696
|
+
const base = owned.length ? [...owned, ...admins] : await adminDevices()
|
|
1697
|
+
return conCoselladores(dedup(base), co)
|
|
1675
1698
|
}
|
|
1676
1699
|
const pub = owner.slice(owner.indexOf(':') + 1)
|
|
1677
1700
|
const m = await memberOf(pub)
|
|
1678
1701
|
// El cajón propio de un aparato de SERVICIO es tan suyo como el de su ns.
|
|
1679
|
-
if (m?.cn) return conCoselladores([m], co)
|
|
1680
|
-
return conCoselladores([...(m ? [m] : []), ...await adminDevices()], co)
|
|
1702
|
+
if (m?.cn) return conCoselladores(dedup([m, ...admins]), co)
|
|
1703
|
+
return conCoselladores(dedup([...(m ? [m] : []), ...await adminDevices()]), co)
|
|
1681
1704
|
}
|
|
1682
1705
|
|
|
1683
1706
|
/**
|
|
@@ -2052,9 +2075,9 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2052
2075
|
*
|
|
2053
2076
|
* Lo de dentro lo corrige el repaso al abrir la bóveda, que es para lo que está.
|
|
2054
2077
|
*/
|
|
2055
|
-
async function verificarDestinatarios (owner, wraps) {
|
|
2078
|
+
async function verificarDestinatarios (owner, wraps, isPublic = false) {
|
|
2056
2079
|
if (!wraps || typeof wraps !== 'object') throw new Error('var.set: the envelope carries no wraps')
|
|
2057
|
-
const { recoveryPub, members } = await secrets.recipientsFor(owner)
|
|
2080
|
+
const { recoveryPub, members } = await secrets.recipientsFor(owner, { public: isPublic })
|
|
2058
2081
|
if (!recoveryPub) throw new Error('var.set: this vault has no recovery key yet')
|
|
2059
2082
|
const deben = new Set(members.map((m) => m.pub))
|
|
2060
2083
|
const traidos = new Set(Object.keys(wraps).filter((k) => k !== RECOVERY_WRAP))
|
|
@@ -2146,7 +2169,10 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2146
2169
|
const out = { drawers: 0, wrapped: 0, dropped: 0, sinLlave: [], failed: [] }
|
|
2147
2170
|
for (const owner of secrets.owners?.() || []) {
|
|
2148
2171
|
const before = new Set(secrets.recipientsIn(owner))
|
|
2149
|
-
|
|
2172
|
+
// POR VISIBILIDAD, no una lista fija: una pública lleva además la envoltura de quien
|
|
2173
|
+
// administra. Con una sola lista se le daría también la de una privada con dueño, que
|
|
2174
|
+
// es justo lo que no puede pasar.
|
|
2175
|
+
const members = (esPublica) => recipientsOf(owner, { public: esPublica })
|
|
2150
2176
|
try {
|
|
2151
2177
|
const r = await secrets.rewrap(owner, members, adminKey, { exact: true })
|
|
2152
2178
|
const after = new Set(secrets.recipientsIn(owner))
|
|
@@ -2179,7 +2205,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2179
2205
|
// Los destinatarios de un cajón no son solo quien lo consume: también quien lo
|
|
2180
2206
|
// administra (`recipientsOf`). Envolver solo para los primeros dejaba a la consola
|
|
2181
2207
|
// sin poder destapar lo que ella misma acababa de saldar.
|
|
2182
|
-
const members =
|
|
2208
|
+
const members = (esPublica) => recipientsOf(owner, { public: esPublica })
|
|
2183
2209
|
try { out[owner] = await spreadKey(owner, members, adminKey) } catch (e) {
|
|
2184
2210
|
// Sin frase, el último recurso es pedírselo a quien sí puede: el propio servicio.
|
|
2185
2211
|
let delegated = 0
|
|
@@ -2213,7 +2239,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2213
2239
|
// (dueño, 2026-09-01: «al envolver hay que envolver siempre para todos los selladores,
|
|
2214
2240
|
// para que siempre tengan la info fresca»). Dos listas para la misma pregunta acaban
|
|
2215
2241
|
// siempre así: una se actualiza y la otra no.
|
|
2216
|
-
try { return await spreadKey(owner,
|
|
2242
|
+
try { return await spreadKey(owner, (esPublica) => recipientsOf(owner, { public: esPublica }), null) } catch (_) { return null }
|
|
2217
2243
|
}
|
|
2218
2244
|
async function deleteSecret (ns, key) { const ok = await secrets.delete(ns, key); if (ok) { audit('secret.rm', { ns, key }); scheduleNotice(ns) } return ok }
|
|
2219
2245
|
|
|
@@ -2448,7 +2474,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2448
2474
|
// envolver hay que envolver siempre para todos los selladores, para que siempre
|
|
2449
2475
|
// tengan la info fresca»). Repartiendo solo a los del ns, un cajón tocado por esta
|
|
2450
2476
|
// puerta dejaba a los selladores con la envoltura vieja.
|
|
2451
|
-
await spreadKey(`ns:${m.cn}`,
|
|
2477
|
+
await spreadKey(`ns:${m.cn}`, (esPublica) => recipientsOf(`ns:${m.cn}`, { public: esPublica }), adminKey).catch(async (e) => {
|
|
2452
2478
|
log('[vault] could not hand the key to the new service:', e.message)
|
|
2453
2479
|
// Sin la frase la bóveda no puede envolvérsela… pero un HERMANO suyo sí: otro
|
|
2454
2480
|
// servicio del mismo cajón ya tiene la llave abierta (§8.11). Si contesta, el
|