@dotrino/remote-agent 0.5.1 → 0.5.3
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 +4 -1
- package/package.json +2 -2
- package/src/link.js +48 -4
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Provee lo común — y **solo** eso:
|
|
|
33
33
|
| `@dotrino/remote-agent` | isomórfico | `e2e` (`makeEphemeral`/`deriveKey`/`seal`/`open`) + constantes de protocolo |
|
|
34
34
|
| `@dotrino/remote-agent/agent` | Node | `startRemoteAgent(opts)` |
|
|
35
35
|
| `@dotrino/remote-agent/client` | navegador | `RemoteAgentClient` |
|
|
36
|
-
| `@dotrino/remote-agent/link` | Node | `enroll`, `loadLink`, `saveLink`, `dataDir`, `parseQr`, `identityFromLink`, `clientLink` |
|
|
36
|
+
| `@dotrino/remote-agent/link` | Node | `enroll`, `loadLink`, `saveLink`, `dataDir`, `parseQr`, `identityFromLink`, `clientLink`, `renewLink` |
|
|
37
37
|
| `@dotrino/remote-agent/discover` | navegador | `listAgentsByLabel` |
|
|
38
38
|
|
|
39
39
|
## Uso
|
|
@@ -90,6 +90,9 @@ hace que se exija ese permiso en el cert y queda en el enlace). Sus secretos los
|
|
|
90
90
|
con `fetchSecrets`/`waitForSecrets` de `@dotrino/vault/service` pasando la identidad por
|
|
91
91
|
parámetros (`{ ns, proxyUrl: link.proxy, masterPubkey: link.iss, device, cert, enc }`).
|
|
92
92
|
|
|
93
|
+
Un agente que corre UNA vez (un bot por cron) no tiene el tic de renovación de
|
|
94
|
+
`startRemoteAgent`: que llame a `renewLink(link)` en cada corrida, o su cert muere a los 30 días.
|
|
95
|
+
|
|
93
96
|
### Un agente como CLIENTE de otro agente (Node)
|
|
94
97
|
|
|
95
98
|
Un agente Node también puede hablar con otro agente —un bot que guarda en el node de
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/remote-agent",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Middleware compartido del ecosistema Dotrino para apps de agente remoto cifrado punto a punto (handshake E2E, emparejamiento SAS con vault, revocación, descubrimiento). Lo consumen dotrino-terminal (PTY) y dotrino-ia (chat con IA).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@dotrino/vault": ">=0.30.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@dotrino/identity": "0.
|
|
52
|
+
"@dotrino/identity": "0.60.3",
|
|
53
53
|
"@dotrino/proxy-client": "0.10.1",
|
|
54
54
|
"@dotrino/vault": "0.30.0"
|
|
55
55
|
}
|
package/src/link.js
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
import fs from 'node:fs'
|
|
17
17
|
import path from 'node:path'
|
|
18
18
|
import os from 'node:os'
|
|
19
|
-
import { signWithDevice, pubkeyId } from '@dotrino/identity/capabilities'
|
|
20
|
-
import { requestDevices } from '@dotrino/identity/vault/remote.js'
|
|
19
|
+
import { signWithDevice, pubkeyId, verifyDelegation } from '@dotrino/identity/capabilities'
|
|
20
|
+
import { requestDevices, requestRenew } from '@dotrino/identity/vault/remote.js'
|
|
21
21
|
import { installNodeGlobals } from '../node-globals.js'
|
|
22
22
|
|
|
23
23
|
export function dataDir (name = 'dotrino-remote-agent') {
|
|
@@ -122,14 +122,58 @@ export function identityFromLink (link, { dir = dataDir() } = {}) {
|
|
|
122
122
|
return { paired: true, master: link.iss, proxy, exp: link.cert.exp, deviceId: await pubkeyId(device.publickey) }
|
|
123
123
|
},
|
|
124
124
|
async listVaultDevices () {
|
|
125
|
-
|
|
125
|
+
// SIN `sinceSeq`: un agente no guarda el acta ni la encadena —no hay una sola línea
|
|
126
|
+
// aquí que lea `chain`—, así que pedir el historial es traerse cientos de KB para
|
|
127
|
+
// tirarlos. Y pedirlo con 0 era lo peor de todo: quien no tiene acta previa adopta la
|
|
128
|
+
// actual de un salto («sin-acta-previa»), de modo que la cadena no iba a usarse jamás.
|
|
129
|
+
// Costó tenerlo así: la respuesta llegó a 1,03 MB, el proxio corta el frame a 1 MB y
|
|
130
|
+
// la bóveda se quedaba muda para todo el ecosistema (2026-08-24).
|
|
131
|
+
return requestDevices({ master: link.iss, proxy, device, cert: link.cert })
|
|
126
132
|
}
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
|
|
136
|
+
/** Con menos de esto de vida se pide cert nuevo (7 días; el cert dura 30). */
|
|
137
|
+
export const RENEW_BEFORE_MS = 7 * 24 * 60 * 60 * 1000
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* RENUEVA el cert del enlace si vence pronto y lo persiste. Es lo que hace el agente
|
|
141
|
+
* de larga duración en cada tic (`startRemoteAgent`), sacado aquí para los agentes que
|
|
142
|
+
* corren UNA vez (un bot por cron): sin esto su cert muere a los 30 días y hay que
|
|
143
|
+
* re-emparejar una máquina que nunca dejó de ser tuya.
|
|
144
|
+
*
|
|
145
|
+
* El cert nuevo se VERIFICA antes de guardarlo (misma maestra, misma llave, más vida).
|
|
146
|
+
* Un cert ya vencido no se renueva: ahí toca re-emparejar. No lanza: devuelve qué pasó.
|
|
147
|
+
*
|
|
148
|
+
* @param {object} link el enlace (se muta con el cert nuevo).
|
|
149
|
+
* @param {{ dir?: string, force?: boolean }} [opts]
|
|
150
|
+
* @returns {Promise<{ renewed: boolean, exp: number|null, reason?: string }>}
|
|
151
|
+
*/
|
|
152
|
+
export async function renewLink (link, { dir = dataDir(), force = false } = {}) {
|
|
153
|
+
const exp = link?.cert?.exp
|
|
154
|
+
if (typeof exp !== 'number') return { renewed: false, exp: null, reason: 'no cert' }
|
|
155
|
+
if (exp <= Date.now()) return { renewed: false, exp, reason: 'expired: enroll again' }
|
|
156
|
+
if (!force && exp - Date.now() > RENEW_BEFORE_MS) return { renewed: false, exp, reason: 'not due' }
|
|
157
|
+
installNodeGlobals(dir)
|
|
158
|
+
try {
|
|
159
|
+
const res = await requestRenew({ master: link.iss, proxy: link.proxy || 'wss://proxy.dotrino.com', device: link.device, cert: link.cert })
|
|
160
|
+
const cert = res?.cert
|
|
161
|
+
if (!cert || cert.sub !== link.device.publickey || cert.iss !== link.iss || !(cert.exp > exp)) {
|
|
162
|
+
throw new Error('the vault returned a cert that is not for this machine')
|
|
163
|
+
}
|
|
164
|
+
const v = await verifyDelegation({ cert, expectedSub: link.device.publickey })
|
|
165
|
+
if (!v.ok) throw new Error(v.reason)
|
|
166
|
+
link.cert = cert
|
|
167
|
+
saveLink(dir, link)
|
|
168
|
+
return { renewed: true, exp: cert.exp }
|
|
169
|
+
} catch (e) {
|
|
170
|
+
return { renewed: false, exp, reason: e.message }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
130
174
|
/** El enlace como lo esperan `ContentClient.connect({ link })` y `RemoteAgentClient`. */
|
|
131
175
|
export function clientLink (link, opts) {
|
|
132
176
|
return { id: identityFromLink(link, opts), cert: link.cert, iss: link.iss, proxy: link.proxy || 'wss://proxy.dotrino.com' }
|
|
133
177
|
}
|
|
134
178
|
|
|
135
|
-
export default { dataDir, loadLink, saveLink, parseQr, enroll, identityFromLink, clientLink }
|
|
179
|
+
export default { dataDir, loadLink, saveLink, parseQr, enroll, identityFromLink, clientLink, renewLink }
|