@dotrino/vault 0.81.0 → 0.82.1
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 +21 -0
- package/package.json +2 -2
- package/src/service.js +60 -6
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({ pkg: '@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.
|
|
3
|
+
"version": "0.82.1",
|
|
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",
|
|
@@ -77,6 +77,6 @@
|
|
|
77
77
|
},
|
|
78
78
|
"dependencies": {
|
|
79
79
|
"@dotrino/opaque": "0.1.0",
|
|
80
|
-
"@dotrino/update": "0.
|
|
80
|
+
"@dotrino/update": "0.5.1"
|
|
81
81
|
}
|
|
82
82
|
}
|
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
|
|
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(
|
|
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
|
|
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 ===
|
|
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(
|
|
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(
|
|
785
|
+
if (res.body.ok !== true) throw new Error(`malformed ${op} reply`)
|
|
732
786
|
return { ok: true }
|
|
733
787
|
} finally { client.close() }
|
|
734
788
|
}
|