@dotrino/remote-agent 0.17.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 +1 -1
  2. package/src/agent.js +30 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/remote-agent",
3
- "version": "0.17.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",
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) {
@@ -375,7 +403,7 @@ export async function startRemoteAgent (opts = {}) {
375
403
  const ack = { op: ACK, sid, seph: eph.pub, ceph: data.eph, machine: myPub, publickey: myPub, ts: Date.now() }
376
404
  const { signature: ackSig } = await signWithDevice({ privateJwk: link.device.privateJwk, data: ack })
377
405
 
378
- 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 })
379
407
  session._exp = Date.now() + SESSION_TTL_MS
380
408
  sessions.set(sid, session)
381
409
  audit('session-open', { sid: sid.slice(0, 8), device: (await pubkeyId(chk.device)).slice(0, 8).toUpperCase() })