@dotrino/remote-agent 0.16.0 → 0.18.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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/agent.js +41 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/remote-agent",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
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",
@@ -47,12 +47,12 @@
47
47
  ],
48
48
  "peerDependencies": {
49
49
  "@dotrino/identity": ">=0.92.0",
50
- "@dotrino/proxy-client": ">=0.29.0",
50
+ "@dotrino/proxy-client": ">=0.30.0",
51
51
  "@dotrino/vault": ">=0.64.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@dotrino/identity": "0.105.0",
55
- "@dotrino/proxy-client": "0.29.0",
55
+ "@dotrino/proxy-client": "0.30.0",
56
56
  "@dotrino/vault": "0.75.0"
57
57
  }
58
58
  }
package/src/agent.js CHANGED
@@ -32,12 +32,13 @@ 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) {
@@ -77,6 +105,7 @@ class AgentSession {
77
105
  * @param {()=>void} [opts.onRevoked] se llamó al auto-borrarse por revocación.
78
106
  * @param {()=>void} [opts.onReady] agente listo y escuchando.
79
107
  * @param {boolean} [opts.quiet] sin logs.
108
+ * @param {number} [opts.netLogMs] cada cuánto van al log las estadísticas de red (5 min).
80
109
  * @param {object} [opts.client] transporte ya conectado (SOLO pruebas).
81
110
  *
82
111
  * Devuelve además el **`client`** (el `WebSocketProxyClient` ya conectado e
@@ -374,7 +403,7 @@ export async function startRemoteAgent (opts = {}) {
374
403
  const ack = { op: ACK, sid, seph: eph.pub, ceph: data.eph, machine: myPub, publickey: myPub, ts: Date.now() }
375
404
  const { signature: ackSig } = await signWithDevice({ privateJwk: link.device.privateJwk, data: ack })
376
405
 
377
- const session = new AgentSession({ sid, key, from, device: chk.device, send })
406
+ const session = new AgentSession({ sid, key, from, device: chk.device, send, isDirect: typeof client.isWebRTCOpen === 'function' ? (t) => client.isWebRTCOpen(t) : null })
378
407
  session._exp = Date.now() + SESSION_TTL_MS
379
408
  sessions.set(sid, session)
380
409
  audit('session-open', { sid: sid.slice(0, 8), device: (await pubkeyId(chk.device)).slice(0, 8).toUpperCase() })
@@ -394,8 +423,18 @@ export async function startRemoteAgent (opts = {}) {
394
423
  s._ingest(p.env).catch(() => send(from, { type: ERROR, error: 'sobre inválido' }))
395
424
  }
396
425
 
426
+ // POR DÓNDE VA EL TRÁFICO, AL LOG. Un agente no tiene topbar: sin esto, saber si una
427
+ // sesión subió a WebRTC o sigue dando la vuelta por el proxio pedía una prueba aparte.
428
+ // Lo escribe el pilar (`logStats`), que lee los clientes del proceso; aquí solo se enciende.
429
+ let net = null
430
+ if (!opts.quiet) {
431
+ const { logStats } = await import('@dotrino/proxy-client')
432
+ net = logStats({ label: 'remote-agent', everyMs: opts.netLogMs })
433
+ }
434
+
397
435
  const stop = () => {
398
436
  clearInterval(revTimer); clearInterval(sweeper)
437
+ if (net) net.stop().catch(() => {})
399
438
  for (const s of sessions.values()) s.close()
400
439
  try { client.close() } catch {}
401
440
  }