@dotrino/remote-agent 0.22.0 → 0.22.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.22.0",
3
+ "version": "0.22.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/src/agent.js CHANGED
@@ -130,7 +130,7 @@ export async function startRemoteAgent (opts = {}) {
130
130
  const dir = opts.dir || dataDir()
131
131
  const link = opts.link || loadLink(dir)
132
132
  if (!link?.device?.privateJwk || !link?.cert || !link?.iss) {
133
- throw new Error('esta máquina no está enlazada. Ejecuta primero el enrolamiento (enroll) de tu agente.')
133
+ throw new Error('this machine is not linked: enroll your agent first')
134
134
  }
135
135
  const master = link.iss
136
136
  const myPub = link.device.publickey
@@ -214,7 +214,7 @@ export async function startRemoteAgent (opts = {}) {
214
214
  if (Array.isArray(res.blocked)) takeBlocked(res.blocked)
215
215
  // El acta viaja con la lista. Apuntar su `seq` es lo que dispara la renovación en el
216
216
  // mismo tic si el dueño nos cambió los permisos.
217
- await anotarActa(res.acta)
217
+ await recordActa(res.acta)
218
218
  } catch (e) {
219
219
  if (!opts.quiet) console.error('[remote-agent] could not refresh revocations (using cache):', e.message)
220
220
  }
@@ -270,10 +270,10 @@ export async function startRemoteAgent (opts = {}) {
270
270
  // máquina. Sin esta rama la migración no terminaba nunca — el disparador comparaba
271
271
  // `seq` y un papel viejo no tiene, así que nunca saltaba.
272
272
  // · o el acta que hemos visto va por delante del papel: el dueño cambió permisos.
273
- const legado = typeof link.cert?.seq !== 'number'
274
- const visto = link.actaSeq
275
- const atrasado = typeof visto === 'number' && typeof link.cert?.seq === 'number' && visto > link.cert.seq
276
- if (!legado && !atrasado) return
273
+ const legacy = typeof link.cert?.seq !== 'number'
274
+ const seenSeq = link.actaSeq
275
+ const behind = typeof seenSeq === 'number' && typeof link.cert?.seq === 'number' && seenSeq > link.cert.seq
276
+ if (!legacy && !behind) return
277
277
  try {
278
278
  const res = await vaultRpc(VMSG.RENEW, VMSG.RENEWED, { op: 'renew', publickey: myPub, ts: Date.now() })
279
279
  const cert = res?.cert
@@ -308,7 +308,7 @@ export async function startRemoteAgent (opts = {}) {
308
308
  * mandar un acta inventada y nombrarse miembro. Ahora el acta tiene que encadenar desde la
309
309
  * que ya se dio por buena, o venir firmada por la bóveda con la que se enroló.
310
310
  */
311
- async function anotarActa (acta) {
311
+ async function recordActa (acta) {
312
312
  if (typeof acta?.seq !== 'number') return
313
313
  const r = await adoptRecord({ link, candidate: acta, me: myPub })
314
314
  if (!r.adopt) {
@@ -328,9 +328,9 @@ export async function startRemoteAgent (opts = {}) {
328
328
  * El acta con la que se juzga un papel. Sin ella no se atiende a nadie — y un acta que
329
329
  * nunca se comprobó (la que guardó una versión anterior) cuenta como no tenerla.
330
330
  */
331
- const actaDeConfianza = () => (link.actaTrusted === true && link.acta ? link.acta : null)
332
- const contextoActa = () => {
333
- const acta = actaDeConfianza()
331
+ const trustedActa = () => (link.actaTrusted === true && link.acta ? link.acta : null)
332
+ const actaContext = () => {
333
+ const acta = trustedActa()
334
334
  return acta ? { actaSeq: acta.seq, sealers: sealersOf(acta) } : { actaSeq: null, sealers: null }
335
335
  }
336
336
 
@@ -349,11 +349,11 @@ export async function startRemoteAgent (opts = {}) {
349
349
 
350
350
  async function handleHandshake (from, p) {
351
351
  const { data, signature, cert } = p
352
- if (!data || !signature || !cert) return send(from, { type: ERROR, error: 'handshake incompleto' })
352
+ if (!data || !signature || !cert) return send(from, { type: ERROR, error: 'incomplete handshake' })
353
353
  // FRESCURA anti-replay: el HS debe ser reciente (±5 min); sin esto un relay
354
354
  // malicioso podía reproducir handshakes viejos (sesiones huérfanas / DoS).
355
355
  if (typeof data.ts !== 'number' || Math.abs(Date.now() - data.ts) > 5 * 60 * 1000) {
356
- return send(from, { type: ERROR, error: 'handshake vencido (posible replay, o el reloj del dispositivo está desfasado)' })
356
+ return send(from, { type: ERROR, error: 'stale handshake (possible replay, or the device clock is off)' })
357
357
  }
358
358
  // Manda el acta, no una llave fija: con varias selladoras el papel de un peer puede
359
359
  // venir firmado por otra bóveda del mismo perfil. Sin acta no se atiende — no hay con
@@ -365,17 +365,17 @@ export async function startRemoteAgent (opts = {}) {
365
365
  // sola — y solo si el acta avanzó. Con freno (`catchUp`): cualquiera puede mandar saludos, y
366
366
  // no por eso se le pregunta a la bóveda cada vez.
367
367
  const judge = async () => {
368
- const chk = await verifyChain({ data, signature, cert, expectedScope: SIGN_SCOPE, ...contextoActa(), revoked: revokedSet })
369
- if (!chk.ok) return { error: 'no autorizado: ' + chk.reason }
368
+ const chk = await verifyChain({ data, signature, cert, expectedScope: SIGN_SCOPE, ...actaContext(), revoked: revokedSet })
369
+ if (!chk.ok) return { code: 'unauthorized', reason: chk.reason, error: 'unauthorized: ' + chk.reason }
370
370
  return { chk }
371
371
  }
372
372
  let verdict = await judge()
373
373
  const known = (v) => {
374
- const acta = actaDeConfianza()
374
+ const acta = trustedActa()
375
375
  return !!acta && (acta.members || []).some((m) => m?.pub === v.chk.device) && memberCan(acta, v.chk.device, 'sign')
376
376
  }
377
377
  if ((verdict.error || !known(verdict)) && await catchUp()) verdict = await judge()
378
- if (verdict.error) return send(from, { type: ERROR, error: verdict.error })
378
+ if (verdict.error) return send(from, { type: ERROR, code: verdict.code, reason: verdict.reason, error: verdict.error })
379
379
  const chk = verdict.chk
380
380
  // CERT ∩ ACTA. `verifyChain` comprueba QUIÉN EMITIÓ el papel; que el APARATO siga siendo
381
381
  // isMember y pueda firmar lo dice el acta, y hay que preguntárselo aparte. Sin esto, un
@@ -391,18 +391,18 @@ export async function startRemoteAgent (opts = {}) {
391
391
  // abrir sesión con otro agente. El bot social se pasó así el 1 de septiembre, con un
392
392
  // node de contenido encendido a su lado que le contestaba «ese aparato no firma por este
393
393
  // perfil». Se le pide lo que corresponde: que el acta lo nombre y le reconozca `sign`.
394
- const acta = actaDeConfianza()
394
+ const acta = trustedActa()
395
395
  const isMember = (acta?.members || []).some((m) => m?.pub === chk.device)
396
396
  if (!acta || !isMember || !memberCan(acta, chk.device, 'sign')) {
397
- return send(from, { type: ERROR, error: 'no autorizado: acta — este perfil no reconoce a ese aparato' })
397
+ return send(from, { type: ERROR, code: 'unauthorized', reason: 'not-in-record', error: 'unauthorized: not-in-record' })
398
398
  }
399
399
  // BLOQUEADO por un aprobador: está en el acta y su papel vale, y aun así no entra. Se le
400
400
  // dice con su código, que no es lo mismo que un papel viejo ni que una revocación.
401
401
  if (blockedSet.has(chk.device)) {
402
402
  audit('session-refused', { reason: 'blocked', device: (await pubkeyId(chk.device)).slice(0, 8).toUpperCase() })
403
- return send(from, { type: ERROR, code: 'blocked', error: 'no autorizado: bloqueado — un aprobador bloqueó este aparato; se desbloquea en la bóveda' })
403
+ return send(from, { type: ERROR, code: 'blocked', reason: 'blocked', error: 'unauthorized: blocked' })
404
404
  }
405
- if (data.op !== HS || typeof data.eph !== 'string') return send(from, { type: ERROR, error: 'handshake inválido' })
405
+ if (data.op !== HS || typeof data.eph !== 'string') return send(from, { type: ERROR, error: 'invalid handshake' })
406
406
 
407
407
  const eph = await makeEphemeral()
408
408
  const sid = [...crypto.getRandomValues(new Uint8Array(16))].map((x) => x.toString(16).padStart(2, '0')).join('')
@@ -419,7 +419,7 @@ export async function startRemoteAgent (opts = {}) {
419
419
  sessions.set(sid, session)
420
420
  audit('session-open', { sid: sid.slice(0, 8), device: (await pubkeyId(chk.device)).slice(0, 8).toUpperCase() })
421
421
  send(from, { type: ACK, sid, ack, signature: ackSig, cert: link.cert })
422
- if (!opts.quiet) console.log(`[remote-agent] sesión ${sid.slice(0, 8)} autorizada (device ${chk.device?.slice?.(0, 8) || '?'})`)
422
+ if (!opts.quiet) console.log(`[remote-agent] session ${sid.slice(0, 8)} authorized (device ${chk.device?.slice?.(0, 8) || '?'})`)
423
423
  if (opts.onSession) { try { opts.onSession(session) } catch (e) { if (!opts.quiet) console.error('[remote-agent] onSession:', e.message) } }
424
424
  }
425
425
 
@@ -428,11 +428,11 @@ export async function startRemoteAgent (opts = {}) {
428
428
  // Con `code` y el `sid`: así el cliente sabe que es SU sesión la que se perdió (el agente
429
429
  // se reinició, o venció) y vuelve a saludar solo, en vez de quedarse con un error. Se
430
430
  // comprueba por el código, no por la frase.
431
- if (!s) return send(from, { type: ERROR, code: 'unknown-session', sid: p.sid, error: 'sesión desconocida o expirada' })
431
+ if (!s) return send(from, { type: ERROR, code: 'unknown-session', sid: p.sid, error: 'unknown or expired session' })
432
432
  s._exp = Date.now() + SESSION_TTL_MS
433
433
  s.from = from
434
434
  if (s._gone) { clearTimeout(s._gone); s._gone = null }
435
- s._ingest(p.env).catch(() => send(from, { type: ERROR, error: 'sobre inválido' }))
435
+ s._ingest(p.env).catch(() => send(from, { type: ERROR, error: 'invalid envelope' }))
436
436
  }
437
437
 
438
438
  // LA CONEXIÓN DEL CLIENTE SE FUE: su sesión no se queda 30 minutos. El proxio avisa de que ese
@@ -479,7 +479,7 @@ export async function startRemoteAgent (opts = {}) {
479
479
  if (typeof b.exp === 'number' && Date.now() > b.exp) return
480
480
  if (!await verifyDeviceSig({ publickey: master, data: b, signature: p.signature })) return
481
481
  audit('revoked', { nonce: b.nonce })
482
- if (!opts.quiet) console.log('\n[remote-agent] tu bóveda REVOCÓ esta máquina → borro el enlace y salgo.')
482
+ if (!opts.quiet) console.log('\n[remote-agent] your vault REVOKED this machine → deleting the link and exiting.')
483
483
  try { fs.rmSync(path.join(dir, 'link.json'), { force: true }) } catch (_) {}
484
484
  stop()
485
485
  if (opts.onRevoked) { try { opts.onRevoked() } catch (_) {} }
@@ -502,7 +502,7 @@ export async function startRemoteAgent (opts = {}) {
502
502
  })
503
503
 
504
504
  if (!opts.quiet) {
505
- console.log(`[remote-agent] agente activo · máquina ${myId} · vault ${(await pubkeyId(master)).slice(0, 16)} · proxy ${proxyUrl}`)
505
+ console.log(`[remote-agent] agent running · machine ${myId} · vault ${(await pubkeyId(master)).slice(0, 16)} · proxy ${proxyUrl}`)
506
506
  }
507
507
  if (opts.onReady) { try { opts.onReady({ machine: myPub, machineId: myId, master }) } catch (_) {} }
508
508
 
package/src/client.js CHANGED
@@ -92,7 +92,7 @@ export class RemoteAgentClient {
92
92
  }
93
93
 
94
94
  async connect () {
95
- if (!this.agentPubkey) throw new Error('falta la dirección del agente destino')
95
+ if (!this.agentPubkey) throw new Error('the target agent address is missing')
96
96
  const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
97
97
  // SIEMPRE EL CAMINO MÁS DIRECTO: WebRTC encendido. Estuvo en `false` desde el 0.1.0
98
98
  // porque entonces Node no tenía con qué; se quedó así cuando ya lo tuvo, y dos equipos
@@ -139,7 +139,7 @@ export class RemoteAgentClient {
139
139
  * de antes de volver a saludar) no dice nada nuevo y se ignora.
140
140
  */
141
141
  _onAgentError (p) {
142
- if (p.code !== 'unknown-session') { this._emit('error', Object.assign(new Error(p.error), { code: p.code })); return }
142
+ if (p.code !== 'unknown-session') { this._emit('error', Object.assign(new Error(p.error), { code: p.code, reason: p.reason })); return }
143
143
  if (p.sid !== this.sid) return
144
144
  this._resume()
145
145
  }
@@ -177,9 +177,9 @@ export class RemoteAgentClient {
177
177
  // El token se apunta AQUÍ, en el mismo tic: si el agente es quien abre el canal
178
178
  // directo, su oferta llega justo detrás del ack y tiene que encontrarlo ya.
179
179
  if (p.type === ACK && p.ack && p.ack.ceph === eph.pub) { off(); if (from) this._directPeers.add(from); resolve({ ...p, from }) }
180
- else if (p.type === ERROR) { off(); reject(new Error(p.error)) }
180
+ else if (p.type === ERROR) { off(); reject(Object.assign(new Error(p.error), { code: p.code, reason: p.reason })) }
181
181
  })
182
- setTimeout(() => { off(); reject(new Error('el agente no respondió (¿está corriendo allí?)')) }, 20000)
182
+ setTimeout(() => { off(); reject(new Error('the agent did not reply (is it running there?)')) }, 20000)
183
183
  })
184
184
  this.client.sendByPubkey(this.agentPubkey, { type: HS, data, signature, cert })
185
185
  const res = await acked
@@ -205,9 +205,9 @@ export class RemoteAgentClient {
205
205
  data: res.ack, signature: res.signature, cert: res.cert, trustedIssuer: this.link.iss,
206
206
  actaSeq: acta?.seq ?? null, sealers: acta ? sealersOf(acta) : null
207
207
  })
208
- if (!chk.ok) throw new Error('el agente no está certificado por tu vault: ' + chk.reason)
209
- if (res.ack.machine !== this.agentPubkey) throw new Error('el ack vino de otro agente')
210
- if (res.ack.ceph !== eph.pub || res.ack.sid !== res.sid) throw new Error('ack no corresponde a este handshake')
208
+ if (!chk.ok) throw new Error('the agent is not certified by your vault: ' + chk.reason)
209
+ if (res.ack.machine !== this.agentPubkey) throw new Error('the ack came from another agent')
210
+ if (res.ack.ceph !== eph.pub || res.ack.sid !== res.sid) throw new Error('the ack does not match this handshake')
211
211
 
212
212
  this.sid = res.sid
213
213
  this.key = await deriveKey(eph.privateKey, res.ack.seph, res.sid)
package/src/link.js CHANGED
@@ -117,12 +117,12 @@ export function identityFromLink (link, { dir = dataDir() } = {}) {
117
117
  return {
118
118
  me: { publickey: device.publickey },
119
119
  async signData (data) {
120
- const firmado = await signWithDevice({ privateJwk: device.privateJwk, publickey: device.publickey, data })
120
+ const signed = await signWithDevice({ privateJwk: device.privateJwk, publickey: device.publickey, data })
121
121
  // A NOMBRE DE QUIÉN VA, igual que en el navegador: el `profileId` y la cadena de
122
122
  // selladores que lo prueba (`refreshChain`). Sin cadena la firma vale ante la bóveda
123
123
  // (que conoce el papel) pero no ante un extraño que use `verifySignedFor`.
124
124
  const chain = Array.isArray(link.sealerChain) && link.sealerChain.length ? link.sealerChain : null
125
- return { ...firmado, profileId: chain ? chain[0].profileId : null, chain }
125
+ return { ...signed, profileId: chain ? chain[0].profileId : null, chain }
126
126
  },
127
127
  async getVaultCert () { return { cert: link.cert } },
128
128
  async vaultStatus () {
@@ -167,12 +167,12 @@ export async function renewLink (link, { dir = dataDir(), force = false } = {})
167
167
  // EL PAPEL DEL MODELO VIEJO SE CAMBIA SÍ O SÍ. Lleva `exp` y no `seq`; se acepta por el
168
168
  // repliegue de migración pero muere en su fecha, y con él la máquina. Los demás se
169
169
  // renuevan cuando el acta va por delante — nunca por calendario, que el papel no vence.
170
- const legado = typeof link.cert.seq !== 'number'
171
- if (legado && typeof link.cert.exp === 'number' && link.cert.exp <= Date.now()) {
170
+ const legacy = typeof link.cert.seq !== 'number'
171
+ if (legacy && typeof link.cert.exp === 'number' && link.cert.exp <= Date.now()) {
172
172
  return { renewed: false, reason: 'expired: enroll again' }
173
173
  }
174
- const atrasado = typeof link.actaSeq === 'number' && typeof link.cert.seq === 'number' && link.actaSeq > link.cert.seq
175
- if (!force && !legado && !atrasado) return { renewed: false, seq: link.cert.seq, reason: 'not due' }
174
+ const behind = typeof link.actaSeq === 'number' && typeof link.cert.seq === 'number' && link.actaSeq > link.cert.seq
175
+ if (!force && !legacy && !behind) return { renewed: false, seq: link.cert.seq, reason: 'not due' }
176
176
  installNodeGlobals(dir)
177
177
  try {
178
178
  // `requestRenew` ya JUZGA lo que devuelve (`checkVaultReply` del pilar: acta bien