@dotrino/vaultd 0.86.0 → 0.88.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/admin.js CHANGED
@@ -204,7 +204,9 @@ export function createAdminDesk ({
204
204
  const toScope = typeof data.ns === 'string' && !!data.ns
205
205
  const toDevice = typeof data.pub === 'string' && !!data.pub
206
206
  if (data.op === 'vars') {
207
- const result = await vars.list({ by })
207
+ // QUIÉN PREGUNTA, no solo su nombre: con su llave se le manda la envoltura de
208
+ // cada pública para que pueda abrirla en su casa.
209
+ const result = await vars.list({ by, caller: chk.device })
208
210
  audit('admin.vars', { by })
209
211
  return { ok: true, result }
210
212
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vaultd",
3
- "version": "0.86.0",
3
+ "version": "0.88.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
@@ -1112,12 +1112,12 @@ async function cmdSecret (rest) {
1112
1112
  /**
1113
1113
  * Una variable en la lista: su nombre, y que TIENE valor. Ninguna lo enseña.
1114
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.
1115
+ * Las dos van selladas igual desde 2026-09-02. Lo que las separa es que a quien administra
1116
+ * se le hace la envoltura de las PÚBLICAS y la bóveda le manda su sobre, así que puede
1117
+ * abrirlas; de una privada de un cajón con dueño no tiene envoltura ni recibe sobre.
1119
1118
  */
1120
- const printVar = (k) => console.log(' · %s ••••••%s', k.key, k.public ? ' (pública)' : '')
1119
+ const printVar = (k) => console.log(' · %s %s', k.key,
1120
+ k.public ? `${k.value ?? '••••••'} (pública)` : '••••••')
1121
1121
  /**
1122
1122
  * Por qué no se aplicó. El daemon deja el motivo en el volcado siguiente; sin él lo
1123
1123
  * único que se podía decir era «revisa los logs», que no ayuda a quien acaba de
@@ -1187,7 +1187,7 @@ async function cmdSecret (rest) {
1187
1187
  // QUÉ SIGNIFICA HOY (dueño, 2026-09-02): todas se guardan selladas igual, y `pública`
1188
1188
  // solo dice a quién se le entrega SIN APROBACIÓN. Decir «su valor se puede ver» era
1189
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')
1190
+ console.log('\n(pública) = se entrega sin pedirte aprobación y quien administra puede leerla. Todas van cifradas.\n')
1191
1191
  printPending(d)
1192
1192
  await printNoPassword()
1193
1193
  return
@@ -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
- const wrapAll = async (cek, owner) => {
300
- const members = (await recipients?.(owner)) || []
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.
@@ -613,6 +618,24 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
613
618
  return { gen }
614
619
  },
615
620
 
621
+ /**
622
+ * LA ENTRADA TAL CUAL (sobre, generación, visibilidad) — sin abrir nada.
623
+ *
624
+ * Es lo que hace falta para MANDARLE a alguien una variable que sí puede leer: el sobre
625
+ * viaja cerrado y lo abre en su casa, con su envoltura. Aquí no se descifra.
626
+ */
627
+ entryOf (owner, key) {
628
+ const [kind, k] = splitOwner(owner)
629
+ const bag = kind === 'ns' ? data.ns : data.dev
630
+ return varsOf(bag, k)[key] || null
631
+ },
632
+ /** La envoltura de ESA generación para ESE miembro, o `null` si no la tiene. */
633
+ wrapOf (owner, gen, memberPub) {
634
+ const [kind, k] = splitOwner(owner)
635
+ const bag = kind === 'ns' ? data.ns : data.dev
636
+ return (bag[k]?.keyring || []).find((g) => g.gen === gen)?.wraps?.[memberPub] || null
637
+ },
638
+
616
639
  /**
617
640
  * ¿EXISTE YA ESTA VARIABLE? Sin abrirla ni decir qué vale.
618
641
  *
@@ -631,9 +654,9 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
631
654
  * haya dos sitios respondiendo a «quién puede abrir este cajón» (la lista la sabe el
632
655
  * acta, y de ahí sale `recipients`).
633
656
  */
634
- async recipientsFor (owner) {
657
+ async recipientsFor (owner, { public: isPublic = false } = {}) {
635
658
  await ensureRecovery(null)
636
- const members = (await recipients?.(owner)) || []
659
+ const members = (await recipients?.(owner, { public: isPublic })) || []
637
660
  return {
638
661
  recoveryPub: data.recovery.pub,
639
662
  members: members.filter((m) => m.encPub).map((m) => ({ pub: m.pub, encPub: m.encPub }))
@@ -796,6 +819,13 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
796
819
  return [...Object.keys(data.ns).map((k) => `ns:${k}`), ...Object.keys(data.dev).map((k) => `dev:${k}`)]
797
820
  },
798
821
  /** @private */
822
+ /**
823
+ * @param {Array|((isPublic:boolean)=>Promise<Array>)} members A quién envolver. Puede ser
824
+ * una FUNCIÓN de la visibilidad: desde 2026-09-02 una pública se envuelve además para
825
+ * quien administra, y eso se decide por variable — cada escritura estrena su CEK, así
826
+ * que el llavero tiene una generación por valor. Con una lista fija se aplicaría la
827
+ * misma a todas y se le daría a la consola la envoltura de una privada con dueño.
828
+ */
799
829
  async _rewrap (owner, members, adminKey = null, { exact = false } = {}) {
800
830
  needSealer('re-wrap the key of a drawer')
801
831
  if (needsMigration()) throw new NeedsMigration()
@@ -805,11 +835,15 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
805
835
  const priv = await openRecovery(adminKey)
806
836
  let wrapped = 0
807
837
  const sinLlave = new Set()
838
+ // De qué variable es cada generación: es lo que dice si esa CEK es de una pública.
839
+ const visibilidad = new Map()
840
+ for (const e of Object.values(varsOf(bag, k))) if (e.gen != null) visibilidad.set(e.gen, !!e.pub)
841
+ const paraGen = async (gen) => (typeof members === 'function' ? members(visibilidad.get(gen) === true) : members)
808
842
  for (const g of bag[k].keyring || []) {
809
843
  const w = g.wraps?.[RECOVERY]
810
844
  if (!w) continue
811
845
  const cek = await sealer.openWrapWith(priv, w)
812
- const r = await sealer.wrapFor(cek, members)
846
+ const r = await sealer.wrapFor(cek, await paraGen(g.gen))
813
847
  // `exact`: el llavero queda con lo que dice el acta y NADA más. Es lo que hace
814
848
  // falta para rehacerlo al abrir la bóveda:
815
849
  // · 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, await recipientsOf(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() }
@@ -1358,14 +1358,21 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
1358
1358
  }
1359
1359
 
1360
1360
  const varsDesk = {
1361
- async list () {
1362
- // `listSecrets`/`listDeviceSecrets` ya traen el valor de las públicas y solo de esas:
1363
- // la frontera se decide en un sitio, y lo mismo ve el dueño en su terminal que aquí.
1361
+ async list ({ caller = null } = {}) {
1362
+ // EL VAULT DECIDE QUÉ ENVÍA PARA LEER (dueño, 2026-09-02: «la consola debe tener todos
1363
+ // los sobres; la diferencia es si el vault se los envía para la lectura o no»).
1364
+ //
1365
+ // Tener la envoltura y recibir el sobre son dos cosas: quien administra tiene la
1366
+ // envoltura de cada PÚBLICA, y por eso aquí se le manda el sobre de esas y solo de
1367
+ // esas. Las privadas de un cajón con dueño no se mandan — y aunque se mandaran no
1368
+ // podría abrirlas, porque su envoltura no existe. Dos cerrojos para lo mismo, a
1369
+ // propósito: que uno falle no basta para leerlas.
1370
+ //
1364
1371
  // Se sella la lista ENTERA, no solo los valores: el proxy tampoco tiene por qué
1365
1372
  // aprender cómo se llaman tus variables ni qué servicios corres.
1366
1373
  return {
1367
1374
  enc: await identity.sealContent(JSON.stringify({
1368
- ns: listSecrets(), dev: await listDeviceSecrets(),
1375
+ ns: conSobres(listSecrets(), 'ns', caller), dev: conSobres(await listDeviceSecrets(), 'dev', caller),
1369
1376
  // Aparatos que están en el acta pero no pueden abrir lo suyo: hay que
1370
1377
  // enseñarlo donde se administra, no solo en el log del servicio.
1371
1378
  incomplete: await incompleteMembers(),
@@ -1386,8 +1393,12 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
1386
1393
  * (`recipientsOf`), y tener dos sitios respondiendo a «quién puede abrir este cajón» es
1387
1394
  * exactamente cómo se acaba dejando fuera a alguien sin que nadie se entere.
1388
1395
  */
1389
- async recipients ({ ns, pub }) {
1390
- return secrets.recipientsFor(ns ? `ns:${ns}` : `dev:${pub}`)
1396
+ async recipients ({ ns, pub, public: isPublic }) {
1397
+ // LA VISIBILIDAD CAMBIA LA LISTA: una pública se envuelve además para quien
1398
+ // administra, así que hay que decirlo al preguntar. Pedirla sin decirlo y luego
1399
+ // guardarla como pública produciría un sobre que la consola no puede abrir — y la
1400
+ // bóveda lo rechazaría, que es lo correcto pero desorienta.
1401
+ return secrets.recipientsFor(ns ? `ns:${ns}` : `dev:${pub}`, { public: !!isPublic })
1391
1402
  },
1392
1403
  /**
1393
1404
  * GUARDAR UNA VARIABLE **SIN VERLA** (dueño, 2026-09-01).
@@ -1416,7 +1427,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
1416
1427
  // despachan o no, son políticas; dales el mismo tratamiento de seguridad»). La
1417
1428
  // marca solo decide si se entrega sin aprobación.
1418
1429
  await verificarAutor(owner, key, sealed, caller)
1419
- await verificarDestinatarios(owner, sealed.wraps)
1430
+ await verificarDestinatarios(owner, sealed.wraps, !!isPublic)
1420
1431
  await secrets.putSealed(owner, key, sealed, { by: who, public: isPublic })
1421
1432
  audit('secret.set', { ns: ns || null, key, sealed: true }); scheduleNotice(ns)
1422
1433
  await settleDebts(owner)
@@ -1456,7 +1467,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
1456
1467
  for (const it of items) {
1457
1468
  if (!it?.key || !it?.sealed) throw new Error('var.setMany: each variable needs its key and its sealed envelope')
1458
1469
  await verificarAutor(owner, it.key, it.sealed, caller)
1459
- await verificarDestinatarios(owner, it.sealed.wraps)
1470
+ await verificarDestinatarios(owner, it.sealed.wraps, !!it.public)
1460
1471
  }
1461
1472
  // Se comprueban TODAS antes de escribir NINGUNA: media carga aplicada es una
1462
1473
  // configuración que nadie quiso, y el servicio se reinicia con ella.
@@ -1660,24 +1671,43 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
1660
1671
  }
1661
1672
 
1662
1673
  /** Sin duplicar: un cosellador puede ser además el dueño del cajón. */
1674
+ /** Sin repetidos por llave: un aparato puede entrar por dos vías (dueño y admin). */
1675
+ const dedup = (miembros) => {
1676
+ const vistos = new Set()
1677
+ return miembros.filter((m) => m && !vistos.has(m.pub) && vistos.add(m.pub))
1678
+ }
1663
1679
  const conCoselladores = (base, co) => {
1664
1680
  const vistos = new Set(base.map((m) => m.pub))
1665
1681
  return [...base, ...co.filter((m) => !vistos.has(m.pub))]
1666
1682
  }
1667
1683
 
1668
- async function recipientsOf (owner) {
1684
+ async function recipientsOf (owner, { public: isPublic = false } = {}) {
1669
1685
  const co = await cosealerMembers()
1686
+ // UNA PÚBLICA SE ENVUELVE TAMBIÉN PARA QUIEN ADMINISTRA (dueño, 2026-09-02: «la consola
1687
+ // debe poder leer todas las variables… debe tener todos los sobres; la diferencia es si
1688
+ // el vault se los envía para la lectura o no» → «cambia la regla y di que se envuelven
1689
+ // las públicas»).
1690
+ //
1691
+ // Puede decidirse variable a variable porque cada escritura estrena su propia CEK: el
1692
+ // llavero guarda una generación por valor, no una por cajón.
1693
+ //
1694
+ // Y la PRIVADA de un cajón con dueño sigue sin envoltura para la consola, que es la
1695
+ // regla de agosto y su motivo no ha cambiado: el token de R2 no tiene por qué poder
1696
+ // abrirse desde un navegador. Lo que cambia es que «pública» ya sí quiere decir algo
1697
+ // para quien administra — puede leerla— además de que se despacha sin aprobación.
1698
+ const admins = isPublic ? await adminDevices() : []
1670
1699
  if (owner.startsWith('ns:')) {
1671
1700
  const owned = await nsMembers(owner.slice(3))
1672
- // Sin dueño (un cajón personal, o uno cuyo servicio ya no está) sí entra quien
1673
- // administra: si no, no quedaría nadie que pudiera abrirlo sin la frase.
1674
- return conCoselladores(owned.length ? owned : await adminDevices(), co)
1701
+ // Sin dueño (un cajón personal, o uno cuyo servicio ya no está) entra quien administra
1702
+ // SIEMPRE: si no, no quedaría nadie que pudiera abrirlo sin la frase.
1703
+ const base = owned.length ? [...owned, ...admins] : await adminDevices()
1704
+ return conCoselladores(dedup(base), co)
1675
1705
  }
1676
1706
  const pub = owner.slice(owner.indexOf(':') + 1)
1677
1707
  const m = await memberOf(pub)
1678
1708
  // 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)
1709
+ if (m?.cn) return conCoselladores(dedup([m, ...admins]), co)
1710
+ return conCoselladores(dedup([...(m ? [m] : []), ...await adminDevices()]), co)
1681
1711
  }
1682
1712
 
1683
1713
  /**
@@ -2052,9 +2082,9 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
2052
2082
  *
2053
2083
  * Lo de dentro lo corrige el repaso al abrir la bóveda, que es para lo que está.
2054
2084
  */
2055
- async function verificarDestinatarios (owner, wraps) {
2085
+ async function verificarDestinatarios (owner, wraps, isPublic = false) {
2056
2086
  if (!wraps || typeof wraps !== 'object') throw new Error('var.set: the envelope carries no wraps')
2057
- const { recoveryPub, members } = await secrets.recipientsFor(owner)
2087
+ const { recoveryPub, members } = await secrets.recipientsFor(owner, { public: isPublic })
2058
2088
  if (!recoveryPub) throw new Error('var.set: this vault has no recovery key yet')
2059
2089
  const deben = new Set(members.map((m) => m.pub))
2060
2090
  const traidos = new Set(Object.keys(wraps).filter((k) => k !== RECOVERY_WRAP))
@@ -2146,7 +2176,10 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
2146
2176
  const out = { drawers: 0, wrapped: 0, dropped: 0, sinLlave: [], failed: [] }
2147
2177
  for (const owner of secrets.owners?.() || []) {
2148
2178
  const before = new Set(secrets.recipientsIn(owner))
2149
- const members = await recipientsOf(owner)
2179
+ // POR VISIBILIDAD, no una lista fija: una pública lleva además la envoltura de quien
2180
+ // administra. Con una sola lista se le daría también la de una privada con dueño, que
2181
+ // es justo lo que no puede pasar.
2182
+ const members = (esPublica) => recipientsOf(owner, { public: esPublica })
2150
2183
  try {
2151
2184
  const r = await secrets.rewrap(owner, members, adminKey, { exact: true })
2152
2185
  const after = new Set(secrets.recipientsIn(owner))
@@ -2179,7 +2212,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
2179
2212
  // Los destinatarios de un cajón no son solo quien lo consume: también quien lo
2180
2213
  // administra (`recipientsOf`). Envolver solo para los primeros dejaba a la consola
2181
2214
  // sin poder destapar lo que ella misma acababa de saldar.
2182
- const members = await recipientsOf(owner)
2215
+ const members = (esPublica) => recipientsOf(owner, { public: esPublica })
2183
2216
  try { out[owner] = await spreadKey(owner, members, adminKey) } catch (e) {
2184
2217
  // Sin frase, el último recurso es pedírselo a quien sí puede: el propio servicio.
2185
2218
  let delegated = 0
@@ -2213,7 +2246,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
2213
2246
  // (dueño, 2026-09-01: «al envolver hay que envolver siempre para todos los selladores,
2214
2247
  // para que siempre tengan la info fresca»). Dos listas para la misma pregunta acaban
2215
2248
  // siempre así: una se actualiza y la otra no.
2216
- try { return await spreadKey(owner, await recipientsOf(owner), null) } catch (_) { return null }
2249
+ try { return await spreadKey(owner, (esPublica) => recipientsOf(owner, { public: esPublica }), null) } catch (_) { return null }
2217
2250
  }
2218
2251
  async function deleteSecret (ns, key) { const ok = await secrets.delete(ns, key); if (ok) { audit('secret.rm', { ns, key }); scheduleNotice(ns) } return ok }
2219
2252
 
@@ -2301,6 +2334,34 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
2301
2334
  * de esta máquina»: taparlo justo aquí —en la máquina donde vive, delante de su dueño—
2302
2335
  * era lo único que la marca no significaba. La consola remota ya las enseña.
2303
2336
  */
2337
+ /**
2338
+ * LE PONE A CADA PÚBLICA SU SOBRE Y LA ENVOLTURA DE QUIEN PREGUNTA, para que pueda leerla.
2339
+ *
2340
+ * Solo a las públicas: es la política de despacho, y es lo único que cambia entre una y
2341
+ * otra. A una privada no se le manda nada — y aunque se le mandara, quien administra no
2342
+ * tiene su envoltura en un cajón con dueño, así que no la abriría igual.
2343
+ *
2344
+ * Si falta la envoltura de quien pregunta (todavía no se le ha hecho, o el cajón se
2345
+ * escribió antes de que entrara) se manda la variable SIN sobre: se ve que existe y que es
2346
+ * pública, y no se puede leer. Eso es cierto y se arregla abriendo la bóveda; inventarse
2347
+ * un valor o esconder la fila sería peor.
2348
+ */
2349
+ const conSobres = (lista, tipo, caller) => {
2350
+ if (!caller) return lista
2351
+ const dar = (owner, k) => {
2352
+ if (!k.public) return k
2353
+ const e = secrets.entryOf?.(owner, k.key)
2354
+ const wrap = e?.gen != null ? secrets.wrapOf?.(owner, e.gen, caller) : null
2355
+ return e?.e && wrap ? { ...k, e: e.e, wrap } : k
2356
+ }
2357
+ if (tipo === 'ns') {
2358
+ const out = {}
2359
+ for (const [ns, keys] of Object.entries(lista)) out[ns] = keys.map((k) => dar(`ns:${ns}`, k))
2360
+ return out
2361
+ }
2362
+ return lista.map((row) => ({ ...row, keys: row.keys.map((k) => dar(`dev:${row.pub}`, k)) }))
2363
+ }
2364
+
2304
2365
  function listSecrets () {
2305
2366
  // YA NO SE ENSEÑA EL VALOR DE UNA PÚBLICA (dueño, 2026-09-02). Desde que todas van en
2306
2367
  // sobre, «pública» dejó de significar «en claro»: dice a quién se le despacha sin
@@ -2448,7 +2509,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
2448
2509
  // envolver hay que envolver siempre para todos los selladores, para que siempre
2449
2510
  // tengan la info fresca»). Repartiendo solo a los del ns, un cajón tocado por esta
2450
2511
  // puerta dejaba a los selladores con la envoltura vieja.
2451
- await spreadKey(`ns:${m.cn}`, await recipientsOf(`ns:${m.cn}`), adminKey).catch(async (e) => {
2512
+ await spreadKey(`ns:${m.cn}`, (esPublica) => recipientsOf(`ns:${m.cn}`, { public: esPublica }), adminKey).catch(async (e) => {
2452
2513
  log('[vault] could not hand the key to the new service:', e.message)
2453
2514
  // Sin la frase la bóveda no puede envolvérsela… pero un HERMANO suyo sí: otro
2454
2515
  // servicio del mismo cajón ya tiene la llave abierta (§8.11). Si contesta, el