@dotrino/remote-agent 0.18.0 → 0.18.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/remote-agent",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
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,7 +27,7 @@ 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`. */
@@ -88,6 +88,7 @@ class AgentSession {
88
88
  close () {
89
89
  if (this._closed) return
90
90
  this._closed = true
91
+ clearTimeout(this._gone)
91
92
  for (const h of this._h.close) { try { h() } catch (_) {} }
92
93
  }
93
94
  }
@@ -105,6 +106,7 @@ class AgentSession {
105
106
  * @param {()=>void} [opts.onRevoked] se llamó al auto-borrarse por revocación.
106
107
  * @param {()=>void} [opts.onReady] agente listo y escuchando.
107
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`).
108
110
  * @param {number} [opts.netLogMs] cada cuánto van al log las estadísticas de red (5 min).
109
111
  * @param {object} [opts.client] transporte ya conectado (SOLO pruebas).
110
112
  *
@@ -420,9 +422,27 @@ export async function startRemoteAgent (opts = {}) {
420
422
  if (!s) return send(from, { type: ERROR, code: 'unknown-session', sid: p.sid, error: 'sesión desconocida o expirada' })
421
423
  s._exp = Date.now() + SESSION_TTL_MS
422
424
  s.from = from
425
+ if (s._gone) { clearTimeout(s._gone); s._gone = null }
423
426
  s._ingest(p.env).catch(() => send(from, { type: ERROR, error: 'sobre inválido' }))
424
427
  }
425
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
+
426
446
  // POR DÓNDE VA EL TRÁFICO, AL LOG. Un agente no tiene topbar: sin esto, saber si una
427
447
  // sesión subió a WebRTC o sigue dando la vuelta por el proxio pedía una prueba aparte.
428
448
  // Lo escribe el pilar (`logStats`), que lee los clientes del proceso; aquí solo se enciende.
package/src/discover.js CHANGED
@@ -29,14 +29,22 @@ import { PING, PONG } from '../protocol.js'
29
29
  * `'ia-agent'`). Omitir = todos los que tengan nameOf y no sean `'cli'` (un navegador
30
30
  * enrolado queda como `cli` y no atiende a nadie).
31
31
  * @returns {Promise<Array<{sub:string,label:string,cn:string|null}>>} uno por miembro.
32
+ * @throws `code: 'no-acta'` si ni la bóveda ni lo guardado dan el acta: no se sabe, y no es «ninguno».
32
33
  */
33
34
  export async function listAgentsByLabel (id, label) {
34
35
  // Por su efecto: trae el acta vigente y la adopta. La lista de aparatos que devuelva —si
35
36
  // devuelve alguna— no se mira: quién es de la cuenta lo dice el acta.
36
37
  let acta = null
37
- try { acta = (await id.listVaultDevices())?.acta || null } catch (_) {}
38
- if (!acta) acta = (await id.profileActa?.().catch(() => null))?.acta || null
39
- if (!acta) return []
38
+ let why = null
39
+ try { acta = (await id.listVaultDevices())?.acta || null } catch (e) { why = e }
40
+ // La bóveda no contestó: vale el acta que ya se tiene guardada.
41
+ if (!acta) acta = (await id.profileActa?.().catch((e) => { why = why || e; return null }))?.acta || null
42
+ // SIN ACTA NO SE SABE quién es de la cuenta, y eso no es «no hay nadie». Devolver una lista
43
+ // vacía aquí hacía que una máquina con la bóveda inalcanzable dijera «no tienes otras
44
+ // máquinas» con toda tranquilidad. Se para y se dice.
45
+ if (!acta) {
46
+ throw Object.assign(new Error(`could not get the account record (acta) from the vault${why ? `: ${why.message}` : ''}`), { code: 'no-acta', cause: why || undefined })
47
+ }
40
48
 
41
49
  const mine = id.me?.publickey
42
50
  const nameOf = (m) => m.cn || m.label || null