@dotrino/vault 0.81.0 → 0.82.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/README.md CHANGED
@@ -282,6 +282,27 @@ await reportUpdated({ product: '@dotrino/terminal-agent', version: '1.3.0', from
282
282
  pendiente y el resultado no llegó (`code: 'unanswered'`). Que nadie conteste nunca es un sí.
283
283
  - `reportUpdated` lanza con `code: 'vault-no-reply'` si la bóveda no contesta.
284
284
 
285
+ **La receta completa**, para un agente que usa `watchSelfUpdateNpm` de `@dotrino/update/npm`:
286
+ `vaultUpdateHooks` devuelve los tres ganchos ya armados (preguntar, avisar que se actualizó,
287
+ avisar que necesita permisos de administrador).
288
+
289
+ ```js
290
+ import { vaultUpdateHooks } from '@dotrino/vault/service'
291
+ watchSelfUpdateNpm({ name: '@dotrino/terminal-agent', /* … */ ...vaultUpdateHooks({ product: '@dotrino/terminal-agent', dir, log }) })
292
+ ```
293
+
294
+ **Si el servicio no puede instalarse solo** porque necesita permisos de administrador (está
295
+ instalado para todo el sistema y no como usuario), no instala ni pregunta: avisa.
296
+
297
+ ```js
298
+ import { reportUpdateNeedsRoot } from '@dotrino/vault/service'
299
+ await reportUpdateNeedsRoot({ product: '@dotrino/terminal-agent', version: '1.3.0', from: '1.2.9', dir }) // → { ok: true }
300
+ ```
301
+
302
+ La bóveda se lo cuenta a quien aprueba («tiene una actualización pendiente: instálala a mano
303
+ en esa máquina»). Mismos errores que `reportUpdated`; avisar una vez por versión es cosa de
304
+ quien llama. Lo preferible es instalar los servicios como usuario, que no necesita nada de esto.
305
+
285
306
  ## Modelo de aprobación (seguro por diseño)
286
307
 
287
308
  - El **dispositivo** que se enrola genera un **código aleatorio** (`makePairingCode`) y
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vault",
3
- "version": "0.81.0",
3
+ "version": "0.82.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
@@ -707,28 +707,82 @@ export async function askUpdateApproval ({ product, version, from = null, dir, p
707
707
  * @param {object} o `product`, `version`, `from` y la conexión, como en `askUpdateApproval`
708
708
  * @returns {Promise<{ ok: true }>}
709
709
  */
710
- export async function reportUpdated ({ product, version, from = null, dir, proxyUrl, masterPubkey, device, cert, timeoutMs = 30000 } = {}) {
710
+ export function reportUpdated (o = {}) { return reportToVault('update-done', o) }
711
+
712
+ /**
713
+ * HAY VERSIÓN NUEVA Y NO PUEDO INSTALARLA SOLO: necesita permisos de administrador (el
714
+ * servicio está instalado para todo el sistema, no como usuario). El aparato se lo dice a su
715
+ * bóveda y ella avisa a quien aprueba, que es quien puede ir a esa máquina e instalarla a
716
+ * mano (dueño, 2026-10-08). No instala nada ni pide permiso: solo avisa.
717
+ *
718
+ * Mismos parámetros y errores que `reportUpdated`. Avisar una sola vez por versión es cosa
719
+ * de quien llama.
720
+ * @returns {Promise<{ ok: true }>}
721
+ */
722
+ export function reportUpdateNeedsRoot (o = {}) { return reportToVault('update-needs-root', o) }
723
+
724
+ /**
725
+ * LOS TRES GANCHOS DE ACTUALIZACIÓN, YA ARMADOS para pasárselos a `watchSelfUpdateNpm` de
726
+ * `@dotrino/update/npm`: una pieza, para que cada agente no lo escriba por su cuenta. No
727
+ * depende de ese paquete: solo devuelve funciones con la forma que él espera.
728
+ *
729
+ * · `mayUpdate({ version, from })` → pregunta a la bóveda y devuelve si se puede. Si la
730
+ * bóveda NO CONTESTA, lanza: para quien llama eso es «no pude preguntar» y se reintenta.
731
+ * Si el pedido VENCE sin respuesta devuelve `false`: vencido es no, y no se repite.
732
+ * · `onUpdated({ version, from })` → le dice a la bóveda que se actualizó. Deja pasar el
733
+ * error, para que quien llama conserve su marcador y lo reintente.
734
+ * · `onNeedsRoot({ version, from })` → le dice que hay versión nueva y necesita permisos de
735
+ * administrador. Igual.
736
+ *
737
+ * @param {object} o
738
+ * @param {string} o.product nombre npm de la pieza
739
+ * @param {(m: string) => void} [o.log]
740
+ * …y la conexión como en los demás: `dir`, o `proxyUrl` + `masterPubkey` + `device` + `cert`.
741
+ * @returns {{ mayUpdate: (u: { version: string, from?: string }) => Promise<boolean>,
742
+ * onUpdated: (u: { version: string, from?: string }) => Promise<void>,
743
+ * onNeedsRoot: (u: { version: string, from?: string }) => Promise<void> }}
744
+ */
745
+ export function vaultUpdateHooks ({ product, log = () => {}, dir, proxyUrl, masterPubkey, device, cert, _ask = askUpdateApproval, _done = reportUpdated, _root = reportUpdateNeedsRoot } = {}) {
746
+ if (typeof product !== 'string' || !product) throw new Error('vaultUpdateHooks: needs product')
747
+ const conn = { dir, proxyUrl, masterPubkey, device, cert }
748
+ return {
749
+ async mayUpdate ({ version, from = null } = {}) {
750
+ let r
751
+ try { r = await _ask({ ...conn, product, version, from, onPending: () => log(`[update] ${product} ${version}: waiting for approval on your vault`) }) } catch (e) {
752
+ if (e?.code === 'unanswered') { log(`[update] ${product} ${version}: nobody answered in time, so it is a no`); return false }
753
+ throw e
754
+ }
755
+ log(`[update] ${product} ${version}: ${r.ok ? (r.asked ? 'approved' : 'no approval needed') : 'not approved'}`)
756
+ return r.ok === true
757
+ },
758
+ async onUpdated ({ version, from = null } = {}) { await _done({ ...conn, product, version, from }) },
759
+ async onNeedsRoot ({ version, from = null } = {}) { await _root({ ...conn, product, version, from }) }
760
+ }
761
+ }
762
+
763
+ /** Lo común de los dos avisos: firmar, mandar, y comprobar quién contesta. */
764
+ async function reportToVault (op, { product, version, from = null, dir, proxyUrl, masterPubkey, device, cert, timeoutMs = 30000 } = {}) {
711
765
  const saved = dir ? readServiceIdentity(dir) : null
712
766
  proxyUrl = proxyUrl || saved?.proxy
713
767
  masterPubkey = masterPubkey || saved?.iss
714
768
  device = device || saved?.device
715
769
  cert = cert || saved?.cert
716
- if (typeof product !== 'string' || !product || typeof version !== 'string' || !version) throw new Error('reportUpdated: needs product and version')
770
+ if (typeof product !== 'string' || !product || typeof version !== 'string' || !version) throw new Error(`${op}: needs product and version`)
717
771
  if (!proxyUrl || !masterPubkey || !device || !cert) throw new Error('service not enrolled')
718
772
  const client = await freshClient(proxyUrl, 20000, masterPubkey)
719
773
  try {
720
774
  await identifyAsService(client, device)
721
- const data = { op: 'update-done', product, version, ...(typeof from === 'string' && from ? { from } : {}), publickey: device.publickey, ts: Date.now() }
775
+ const data = { op, product, version, ...(typeof from === 'string' && from ? { from } : {}), publickey: device.publickey, ts: Date.now() }
722
776
  const { signature } = await signWithDevice({ privateJwk: device.privateJwk, data })
723
- const pending = waitForMsg(client, (p) => p.type === MSG.ERROR || (p.type === MSG.SECRETS_RESULT && p.body?.op === 'update-done' && p.body.product === product && p.body.version === version), timeoutMs)
777
+ const pending = waitForMsg(client, (p) => p.type === MSG.ERROR || (p.type === MSG.SECRETS_RESULT && p.body?.op === op && p.body.product === product && p.body.version === version), timeoutMs)
724
778
  client.sendByPubkey(masterPubkey, { type: MSG.SECRETS, data, signature, cert })
725
779
  const res = await pending.catch((e) => {
726
- if (/timeout/.test(e.message)) throw Object.assign(new Error('update-done: the vault did not answer'), { code: 'vault-no-reply' })
780
+ if (/timeout/.test(e.message)) throw Object.assign(new Error(`${op}: the vault did not answer`), { code: 'vault-no-reply' })
727
781
  throw e
728
782
  })
729
783
  if (res.type === MSG.ERROR) throw vaultError(res)
730
784
  await verifyResponder({ body: res.body, seal: res.seal, acta: res.acta, masterPubkey, knownSeq: saved?.actaSeq ?? null })
731
- if (res.body.ok !== true) throw new Error('malformed update-done reply')
785
+ if (res.body.ok !== true) throw new Error(`malformed ${op} reply`)
732
786
  return { ok: true }
733
787
  } finally { client.close() }
734
788
  }