@dotrino/vault 0.47.0 → 0.48.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.
@@ -186,7 +186,13 @@ async function cmdRun () {
186
186
  const cmd = sep >= 0 ? argv.slice(sep + 1) : []
187
187
  if (!cmd.length) { console.error('uso: dotrino-env run --vault <bóveda> [--ns <ns>] -- <cmd> [args…]'); process.exit(2) }
188
188
  const onPending = () => console.error('[dotrino-env] waiting for approval on your phone…')
189
- const { injected, overridden } = await loadEnv({ ns: flag('ns'), vault: etiquetaBoveda(), onPending })
189
+ // POR QUÉ SE REINTENTA, SIEMPRE. Sin esto la espera es muda: la bóveda contestaba, la
190
+ // respuesta se caía por un motivo concreto —un aparato sin sobre en su cajón, por
191
+ // ejemplo— y por fuera solo se veía «waiting for approval» una y otra vez, aunque el
192
+ // dueño hubiera aprobado cada una. Un reintento que no dice su causa es un fallback: le
193
+ // pone cara de espera a un error. (Ver CLAUDE.md, «nada de repliegues».)
194
+ const onRetry = (e, ms) => console.error('[dotrino-env] failed: %s — retrying in %ds', e.message, Math.round(ms / 1000))
195
+ const { injected, overridden } = await loadEnv({ ns: flag('ns'), vault: etiquetaBoveda(), onPending, onRetry })
190
196
  console.error('[dotrino-env] %d valor(es) en el entorno de %s%s', injected.length, cmd[0],
191
197
  overridden.length ? ` (pisados: ${overridden.join(', ')})` : '')
192
198
  const child = spawn(cmd[0], cmd.slice(1), { stdio: 'inherit', env: process.env })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vault",
3
- "version": "0.47.0",
3
+ "version": "0.48.0",
4
4
  "description": "Usa ESTE dispositivo (navegador) como bóveda/CA del ecosistema Dotrino: atiende enrolamientos por el proxy y firma certificados de delegación a tus máquinas. Incluye el cliente de SERVICIO (Node): un proyecto se enrola una vez y jala sus credenciales del vault en vez del .env (`import '@dotrino/vault/config'`).",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/service.js CHANGED
@@ -666,7 +666,17 @@ async function openSealedBundle (sealed, ident, acta = null, masterPubkey = null
666
666
  const gen = e.gen ?? e.e?.gen ?? 0
667
667
  await comprobarFirma(e.owner, key, gen, e.e, e.seal)
668
668
  const cek = await cekDe(cual, gen)
669
- if (!cek) throw new Error(`no key to open ${key}: this device has no wrapping for its drawer`)
669
+ if (!cek) {
670
+ // NO ES UN TROPIEZO: ES UN ESTADO QUE HAY QUE ARREGLAR EN LA BÓVEDA.
671
+ //
672
+ // Un aparato que entra después de escrita una variable no tiene envoltura de ella, y
673
+ // NO PUEDE tenerla: envolver exige abrir la llave del cajón, y eso solo pasa cuando
674
+ // el dueño abre la bóveda. Reintentar no la crea. Lleva `code` porque el mensaje lo
675
+ // empareja otra capa y una frase no es un contrato (ver `dotrino-error-strings`).
676
+ throw Object.assign(
677
+ new Error(`no key to open ${key}: this device has no wrapping in its drawer — open the vault (dotrino-vault unlock) so it hands it the key`),
678
+ { code: 'no-wrapping' })
679
+ }
670
680
  out[key] = await decryptWithCek({ cek, envelope: e.e })
671
681
  }
672
682
  return out
@@ -991,16 +1001,34 @@ function fingerprintOf (secrets) {
991
1001
  * servicio no opera hasta que esto resuelva — esa es la regla.
992
1002
  * @returns {Promise<Record<string,string>>}
993
1003
  */
1004
+ /**
1005
+ * ¿ESTE FALLO SE ARREGLA SOLO ESPERANDO? Si no, reintentar es peor que rendirse.
1006
+ *
1007
+ * Lo NO transitorio exige que alguien haga algo: falta de enrolamiento, cert revocado o
1008
+ * vencido, scope equivocado → re-emparejar. Que te lo DENIEGUEN tampoco se reintenta: fue
1009
+ * una decisión, no un tropiezo.
1010
+ *
1011
+ * Y no tener ENVOLTURA en el cajón (`no-wrapping`) es el que costó una tarde. La bóveda
1012
+ * pedía aprobación, el teléfono timbraba, el dueño aprobaba, la bóveda contestaba un bundle
1013
+ * que este aparato no podía abrir, y el agente volvía a pedir — así que el teléfono
1014
+ * timbraba otra vez. «Sigo aprobando y aprobando», textual. Ninguna aprobación iba a crear
1015
+ * esa envoltura: envolver exige abrir la llave del cajón, o sea ABRIR la bóveda.
1016
+ *
1017
+ * Se decide por `e.code` donde lo hay: emparejar un mensaje por su texto se rompe en
1018
+ * silencio en cuanto alguien lo reescribe (ver `dotrino-error-strings`).
1019
+ */
1020
+ export function isFinal (e) {
1021
+ if (e?.code === 'no-wrapping') return true
1022
+ return /not enrolled|invalid ns|unauthorized: (revoked|expired|scope|cn|untrusted-issuer|cert-device-mismatch|denied)/.test(e?.message || '')
1023
+ }
1024
+
994
1025
  export async function waitForSecrets ({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, retryMs = 5000, maxRetryMs = 60000, onRetry, onPending, onCert } = {}) {
995
1026
  let delay = retryMs
996
1027
  for (;;) {
997
1028
  try {
998
1029
  return await fetchSecrets({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, onPending, onCert })
999
1030
  } catch (e) {
1000
- // Lo NO transitorio no se arregla reintentando: falta de enrolamiento,
1001
- // cert revocado/vencido o scope equivocado exigen re-emparejar → se corta.
1002
- // Que te lo DENIEGUEN tampoco se reintenta: fue una decisión, no un tropiezo.
1003
- if (/not enrolled|invalid ns|unauthorized: (revoked|expired|scope|cn|untrusted-issuer|cert-device-mismatch|denied)/.test(e.message)) throw e
1031
+ if (isFinal(e)) throw e
1004
1032
  try { onRetry?.(e, delay) } catch (_) {}
1005
1033
  await new Promise((r) => setTimeout(r, delay))
1006
1034
  delay = Math.min(maxRetryMs, Math.round(delay * 1.6))