@dotrino/remote-agent 0.17.0 → 0.18.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/package.json +1 -1
- package/protocol.js +5 -0
- package/src/agent.js +51 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/remote-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
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",
|
package/protocol.js
CHANGED
|
@@ -39,6 +39,11 @@ export const SIGN_SCOPE = 'vault:sign'
|
|
|
39
39
|
/** Vigencia de una sesión inactiva y refresco de la lista de revocados. */
|
|
40
40
|
export const SESSION_TTL_MS = 30 * 60 * 1000
|
|
41
41
|
export const REVOKE_REFRESH_MS = 5 * 60 * 1000
|
|
42
|
+
/**
|
|
43
|
+
* Cuánto se espera a un cliente cuya conexión se fue antes de cerrarle la sesión. Un cliente
|
|
44
|
+
* que reconecta sigue con su sesión desde un token nuevo: si en este plazo manda algo, se queda.
|
|
45
|
+
*/
|
|
46
|
+
export const SESSION_GONE_MS = 20 * 1000
|
|
42
47
|
|
|
43
48
|
/**
|
|
44
49
|
* RENOVACIÓN del cert de esta máquina. El cert dura 30 días; se pide uno fresco
|
package/src/agent.js
CHANGED
|
@@ -27,17 +27,18 @@ import { adoptRecord } from './record.js'
|
|
|
27
27
|
import { installNodeGlobals } from '../node-globals.js'
|
|
28
28
|
import { makeEphemeral, deriveKey, seal, open } from '../e2e.js'
|
|
29
29
|
import { makeCatchUp } from './catch-up.js'
|
|
30
|
-
import { HS, ACK, DATA, PING, PONG, ERROR, VMSG, SIGN_SCOPE, SESSION_TTL_MS, REVOKE_REFRESH_MS } from '../protocol.js'
|
|
30
|
+
import { HS, ACK, DATA, PING, PONG, ERROR, VMSG, SIGN_SCOPE, SESSION_TTL_MS, SESSION_GONE_MS, REVOKE_REFRESH_MS } from '../protocol.js'
|
|
31
31
|
import { loadLink, saveLink, dataDir } from './link.js'
|
|
32
32
|
|
|
33
33
|
/** Sesión cifrada con un dispositivo cliente. La app recibe esto en `onSession`. */
|
|
34
34
|
class AgentSession {
|
|
35
|
-
constructor ({ sid, key, from, device, send }) {
|
|
35
|
+
constructor ({ sid, key, from, device, send, isDirect = null }) {
|
|
36
36
|
this.sid = sid
|
|
37
37
|
this.key = key
|
|
38
38
|
this.from = from // token del cliente en el proxy
|
|
39
39
|
this.device = device // pubkey del dispositivo cliente
|
|
40
40
|
this._send = send // (to, obj) => void
|
|
41
|
+
this._isDirect = isDirect // (token) => boolean, o null si el transporte no lo sabe
|
|
41
42
|
this._h = { message: [], close: [] }
|
|
42
43
|
this._closed = false
|
|
43
44
|
}
|
|
@@ -49,6 +50,33 @@ class AgentSession {
|
|
|
49
50
|
if (env) this._send(this.from, { type: DATA, sid: this.sid, env })
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* ¿VA YA POR EL CANAL DIRECTO (WebRTC) lo que se le mande a este cliente? Mientras sea
|
|
55
|
+
* `false`, cada `send` da la vuelta por el proxio. Sirve para que la app decida qué manda
|
|
56
|
+
* YA y qué puede esperar: nunca para dejar de mandar (subir de escalón no bloquea).
|
|
57
|
+
*/
|
|
58
|
+
direct () {
|
|
59
|
+
if (this._closed || !this._isDirect) return false
|
|
60
|
+
try { return !!this._isDirect(this.from) } catch (_) { return false }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Espera a que abra el canal directo, como mucho `ms`. Resuelve `true` si abrió y `false`
|
|
65
|
+
* si se acabó el plazo o la sesión se cerró — en los dos casos se sigue, por donde haya.
|
|
66
|
+
*/
|
|
67
|
+
whenDirect (ms = 8000) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const until = Date.now() + ms
|
|
70
|
+
const look = () => {
|
|
71
|
+
if (this.direct()) return resolve(true)
|
|
72
|
+
if (this._closed || Date.now() >= until) return resolve(false)
|
|
73
|
+
const t = setTimeout(look, 100)
|
|
74
|
+
t.unref?.()
|
|
75
|
+
}
|
|
76
|
+
look()
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
52
80
|
on (ev, cb) { this._h[ev]?.push(cb); return this }
|
|
53
81
|
|
|
54
82
|
async _ingest (env) {
|
|
@@ -60,6 +88,7 @@ class AgentSession {
|
|
|
60
88
|
close () {
|
|
61
89
|
if (this._closed) return
|
|
62
90
|
this._closed = true
|
|
91
|
+
clearTimeout(this._gone)
|
|
63
92
|
for (const h of this._h.close) { try { h() } catch (_) {} }
|
|
64
93
|
}
|
|
65
94
|
}
|
|
@@ -77,6 +106,7 @@ class AgentSession {
|
|
|
77
106
|
* @param {()=>void} [opts.onRevoked] se llamó al auto-borrarse por revocación.
|
|
78
107
|
* @param {()=>void} [opts.onReady] agente listo y escuchando.
|
|
79
108
|
* @param {boolean} [opts.quiet] sin logs.
|
|
109
|
+
* @param {number} [opts.sessionGoneMs] cuánto se espera a un cliente cuya conexión se fue (`SESSION_GONE_MS`).
|
|
80
110
|
* @param {number} [opts.netLogMs] cada cuánto van al log las estadísticas de red (5 min).
|
|
81
111
|
* @param {object} [opts.client] transporte ya conectado (SOLO pruebas).
|
|
82
112
|
*
|
|
@@ -375,7 +405,7 @@ export async function startRemoteAgent (opts = {}) {
|
|
|
375
405
|
const ack = { op: ACK, sid, seph: eph.pub, ceph: data.eph, machine: myPub, publickey: myPub, ts: Date.now() }
|
|
376
406
|
const { signature: ackSig } = await signWithDevice({ privateJwk: link.device.privateJwk, data: ack })
|
|
377
407
|
|
|
378
|
-
const session = new AgentSession({ sid, key, from, device: chk.device, send })
|
|
408
|
+
const session = new AgentSession({ sid, key, from, device: chk.device, send, isDirect: typeof client.isWebRTCOpen === 'function' ? (t) => client.isWebRTCOpen(t) : null })
|
|
379
409
|
session._exp = Date.now() + SESSION_TTL_MS
|
|
380
410
|
sessions.set(sid, session)
|
|
381
411
|
audit('session-open', { sid: sid.slice(0, 8), device: (await pubkeyId(chk.device)).slice(0, 8).toUpperCase() })
|
|
@@ -392,9 +422,27 @@ export async function startRemoteAgent (opts = {}) {
|
|
|
392
422
|
if (!s) return send(from, { type: ERROR, code: 'unknown-session', sid: p.sid, error: 'sesión desconocida o expirada' })
|
|
393
423
|
s._exp = Date.now() + SESSION_TTL_MS
|
|
394
424
|
s.from = from
|
|
425
|
+
if (s._gone) { clearTimeout(s._gone); s._gone = null }
|
|
395
426
|
s._ingest(p.env).catch(() => send(from, { type: ERROR, error: 'sobre inválido' }))
|
|
396
427
|
}
|
|
397
428
|
|
|
429
|
+
// LA CONEXIÓN DEL CLIENTE SE FUE: su sesión no se queda 30 minutos. El proxio avisa de que ese
|
|
430
|
+
// token murió (`peer_disconnected`); un token no vuelve, así que quien sigue vivo manda desde
|
|
431
|
+
// otro y la sesión se queda (`handleData`). Si en `SESSION_GONE_MS` no llega nada, se cierra.
|
|
432
|
+
// Antes solo vencía por `SESSION_TTL_MS`: un teléfono que volvía del fondo abría sesión nueva
|
|
433
|
+
// y la vieja seguía enganchada a su consola (seis del mismo aparato en una, el 2026-10-09).
|
|
434
|
+
const goneMs = opts.sessionGoneMs ?? SESSION_GONE_MS
|
|
435
|
+
client.on('peer_disconnected', (token) => {
|
|
436
|
+
for (const [sid, s] of sessions) {
|
|
437
|
+
if (s.from !== token || s._gone) continue
|
|
438
|
+
s._gone = setTimeout(() => {
|
|
439
|
+
s.close(); sessions.delete(sid)
|
|
440
|
+
audit('session-gone', { sid: sid.slice(0, 8) })
|
|
441
|
+
}, goneMs)
|
|
442
|
+
s._gone.unref?.()
|
|
443
|
+
}
|
|
444
|
+
})
|
|
445
|
+
|
|
398
446
|
// POR DÓNDE VA EL TRÁFICO, AL LOG. Un agente no tiene topbar: sin esto, saber si una
|
|
399
447
|
// sesión subió a WebRTC o sigue dando la vuelta por el proxio pedía una prueba aparte.
|
|
400
448
|
// Lo escribe el pilar (`logStats`), que lee los clientes del proceso; aquí solo se enciende.
|