@dotrino/vault 0.52.0 → 0.53.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/LICENSE CHANGED
File without changes
package/README.md CHANGED
File without changes
@@ -206,7 +206,10 @@ async function cmdRun () {
206
206
  // ejemplo— y por fuera solo se veía «waiting for approval» una y otra vez, aunque el
207
207
  // dueño hubiera aprobado cada una. Un reintento que no dice su causa es un fallback: le
208
208
  // pone cara de espera a un error. (Ver CLAUDE.md, «nada de repliegues».)
209
- const onRetry = (e, ms) => console.error('[dotrino-env] failed: %s — retrying in %ds', e.message, Math.round(ms / 1000))
209
+ // El plazo se imprime en minutos cuando lo es: un «retrying in 310s» parece un cuelgue,
210
+ // y no lo es — es el pedido que ya timbró, esperando a vencer antes de volver a sonar.
211
+ const humanDelay = (ms) => (ms >= 90000 ? `${Math.round(ms / 60000)} min` : `${Math.round(ms / 1000)}s`)
212
+ const onRetry = (e, ms) => console.error('[dotrino-env] failed: %s — retrying in %s', e.message, humanDelay(ms))
210
213
  // `--public`: SOLO las variables públicas, y por eso no hace sonar el teléfono de nadie.
211
214
  // La aprobación existe para soltar claves privadas; una pública está guardada en claro y
212
215
  // ya la ve quien administra. Es lo que quiere un arranque que solo necesita configuración
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vault",
3
- "version": "0.52.0",
3
+ "version": "0.53.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/admin.js CHANGED
File without changes
package/src/atrest.js CHANGED
File without changes
package/src/config.js CHANGED
File without changes
package/src/enroll.js CHANGED
File without changes
package/src/env.js CHANGED
File without changes
package/src/envtext.js CHANGED
File without changes
package/src/index.js CHANGED
File without changes
package/src/invite.js CHANGED
File without changes
package/src/kek.js CHANGED
File without changes
package/src/lock.js CHANGED
File without changes
package/src/protocol.js CHANGED
File without changes
package/src/revocation.js CHANGED
File without changes
package/src/sealed.js CHANGED
File without changes
package/src/service.js CHANGED
@@ -668,7 +668,13 @@ async function openSealedBundle (sealed, ident, acta = null, masterPubkey = null
668
668
  // sería aceptar un valor en claro dentro de un paquete que se supone todo sellado, o
669
669
  // sea la puerta para colar uno.
670
670
  if (typeof e.v === 'string' && !e.e) {
671
- throw Object.assign(new Error(`${key} arrived in the clear: since 2026-09-02 every variable travels sealed`), { code: 'plaintext-var' })
671
+ // TAMPOCO SE ARREGLA ESPERANDO, igual que `no-wrapping`. El valor está guardado en
672
+ // claro en la bóveda, de antes de que todo viajara sellado, y solo vuelve a
673
+ // escribirlo quien puede abrirla. Reintentar no lo sella: solo vuelve a timbrar.
674
+ throw Object.assign(
675
+ new Error(`${key} arrived in the clear: since 2026-09-02 every variable travels sealed` +
676
+ ` — rewrite it in the vault (dotrino-vault unlock, then secret set ${ident?.ns || '<ns>'} ${key} …) or remove it`),
677
+ { code: 'plaintext-var' })
672
678
  }
673
679
  // `owner` dice de qué cajón salió, y `gen` con qué llave de ese cajón se abre.
674
680
  const cual = String(e.owner || '').startsWith('dev:') ? 'dev' : 'ns'
@@ -1026,21 +1032,52 @@ function fingerprintOf (secrets) {
1026
1032
  * Se decide por `e.code` donde lo hay: emparejar un mensaje por su texto se rompe en
1027
1033
  * silencio en cuanto alguien lo reescribe (ver `dotrino-error-strings`).
1028
1034
  */
1035
+ const FINAL_CODES = new Set([
1036
+ 'no-wrapping', // falta la envoltura, y solo la fabrica el dueño abriendo la bóveda
1037
+ 'plaintext-var' // el valor está en claro en la bóveda: hay que volver a escribirlo
1038
+ ])
1039
+
1029
1040
  export function isFinal (e) {
1030
- if (e?.code === 'no-wrapping') return true
1041
+ if (FINAL_CODES.has(e?.code)) return true
1031
1042
  return /not enrolled|invalid ns|unauthorized: (revoked|expired|scope|cn|untrusted-issuer|cert-device-mismatch|denied)/.test(e?.message || '')
1032
1043
  }
1033
1044
 
1034
- export async function waitForSecrets ({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, retryMs = 5000, maxRetryMs = 60000, onRetry, onPending, onCert, publicOnly = false } = {}) {
1035
- let delay = retryMs
1045
+ /**
1046
+ * CUÁNTO ESPERAR ANTES DE VOLVER A PEDIR, y por qué no siempre es el backoff.
1047
+ *
1048
+ * Un cajón con aprobación hace sonar el teléfono en CADA petición. Con el reintento a
1049
+ * cinco segundos, un fallo posterior a la aprobación volvía a pedir enseguida y el dueño
1050
+ * veía dos avisos del mismo cajón: aprobaba el primero para nada. La regla es que un
1051
+ * reintento no llegue nunca antes de que venza el pedido que ya está sonando.
1052
+ *
1053
+ * La otra mitad importa igual: si el intento se pasó la ventana entera esperando a que
1054
+ * alguien aprobara, no se le suma otra ventana — ahí ya no hay nada sonando, y hacerle
1055
+ * esperar cinco minutos más sería un cuelgue.
1056
+ *
1057
+ * @param {number} backoffMs lo que tocaría por el backoff normal
1058
+ * @param {number} rangAt cuándo timbró este intento (0 si no timbró)
1059
+ * @param {number} approvalTimeoutMs lo que dura un pedido de aprobación
1060
+ * @param {number} now
1061
+ */
1062
+ export function retryDelay (backoffMs, rangAt, approvalTimeoutMs = APPROVAL_TIMEOUT_MS, now = Date.now()) {
1063
+ if (!rangAt) return backoffMs
1064
+ return Math.max(backoffMs, approvalTimeoutMs - (now - rangAt))
1065
+ }
1066
+
1067
+ export async function waitForSecrets ({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, retryMs = 5000, maxRetryMs = 60000, approvalTimeoutMs = APPROVAL_TIMEOUT_MS, onRetry, onPending, onCert, publicOnly = false } = {}) {
1068
+ let backoff = retryMs
1069
+ let rangAt = 0 // cuándo timbró ESTE intento; 0 = no hubo aprobación de por medio
1070
+ const notePending = (info) => { rangAt = Date.now(); onPending?.(info) }
1036
1071
  for (;;) {
1037
1072
  try {
1038
- return await fetchSecrets({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, onPending, onCert, publicOnly })
1073
+ return await fetchSecrets({ dir, ns, proxyUrl, masterPubkey, device, cert, enc, approvalTimeoutMs, onPending: notePending, onCert, publicOnly })
1039
1074
  } catch (e) {
1040
1075
  if (isFinal(e)) throw e
1041
- try { onRetry?.(e, delay) } catch (_) {}
1042
- await new Promise((r) => setTimeout(r, delay))
1043
- delay = Math.min(maxRetryMs, Math.round(delay * 1.6))
1076
+ const wait = retryDelay(backoff, rangAt, approvalTimeoutMs)
1077
+ rangAt = 0
1078
+ try { onRetry?.(e, wait) } catch (_) {}
1079
+ await new Promise((r) => setTimeout(r, wait))
1080
+ backoff = Math.min(maxRetryMs, Math.round(backoff * 1.6))
1044
1081
  }
1045
1082
  }
1046
1083
  }
package/src/sshAgent.js CHANGED
File without changes
package/src/sshKeys.js CHANGED
File without changes