@dotrino/remote-agent 0.6.1 → 0.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/remote-agent",
3
- "version": "0.6.1",
3
+ "version": "0.7.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",
@@ -45,7 +45,7 @@
45
45
  "p2p"
46
46
  ],
47
47
  "peerDependencies": {
48
- "@dotrino/identity": ">=0.73.0",
48
+ "@dotrino/identity": ">=0.73.1",
49
49
  "@dotrino/proxy-client": ">=0.10.1",
50
50
  "@dotrino/vault": ">=0.30.0"
51
51
  },
package/src/agent.js CHANGED
@@ -178,9 +178,16 @@ export async function startRemoteAgent (opts = {}) {
178
178
  * que una segunda bóveda pueda emitir; lo que se fija es el perfil.
179
179
  */
180
180
  async function renewCertIfNeeded () {
181
- // ¿Nos quedamos atrás? Lo sabemos por el acta que la bóveda manda con cada respuesta.
181
+ // DOS MOTIVOS PARA PEDIR PAPEL NUEVO:
182
+ // · el nuestro es del MODELO VIEJO (lleva `exp` y no `seq`). Ese hay que cambiarlo sí
183
+ // o sí: se acepta por el repliegue de migración, pero muere en su fecha y con él la
184
+ // máquina. Sin esta rama la migración no terminaba nunca — el disparador comparaba
185
+ // `seq` y un papel viejo no tiene, así que nunca saltaba.
186
+ // · o el acta que hemos visto va por delante del papel: el dueño cambió permisos.
187
+ const legado = typeof link.cert?.seq !== 'number'
182
188
  const visto = link.actaSeq
183
- if (typeof visto !== 'number' || typeof link.cert?.seq !== 'number' || visto <= link.cert.seq) return
189
+ const atrasado = typeof visto === 'number' && typeof link.cert?.seq === 'number' && visto > link.cert.seq
190
+ if (!legado && !atrasado) return
184
191
  try {
185
192
  const res = await vaultRpc(VMSG.RENEW, VMSG.RENEWED, { op: 'renew', publickey: myPub, ts: Date.now() })
186
193
  const cert = res?.cert
package/src/link.js CHANGED
@@ -17,6 +17,7 @@ import fs from 'node:fs'
17
17
  import path from 'node:path'
18
18
  import os from 'node:os'
19
19
  import { signWithDevice, pubkeyId, verifyDelegation } from '@dotrino/identity/capabilities'
20
+ import { sealersOf } from '@dotrino/identity/acta'
20
21
  import { requestDevices, requestRenew } from '@dotrino/identity/vault/remote.js'
21
22
  import { installNodeGlobals } from '../node-globals.js'
22
23
 
@@ -150,24 +151,39 @@ export const RENEW_BEFORE_MS = 7 * 24 * 60 * 60 * 1000
150
151
  * @returns {Promise<{ renewed: boolean, exp: number|null, reason?: string }>}
151
152
  */
152
153
  export async function renewLink (link, { dir = dataDir(), force = false } = {}) {
153
- const exp = link?.cert?.exp
154
- if (typeof exp !== 'number') return { renewed: false, exp: null, reason: 'no cert' }
155
- if (exp <= Date.now()) return { renewed: false, exp, reason: 'expired: enroll again' }
156
- if (!force && exp - Date.now() > RENEW_BEFORE_MS) return { renewed: false, exp, reason: 'not due' }
154
+ if (!link?.cert) return { renewed: false, seq: null, reason: 'no cert' }
155
+ // EL PAPEL DEL MODELO VIEJO SE CAMBIA SÍ O SÍ. Lleva `exp` y no `seq`; se acepta por el
156
+ // repliegue de migración pero muere en su fecha, y con él la máquina. Los demás se
157
+ // renuevan cuando el acta va por delante — nunca por calendario, que el papel no vence.
158
+ const legado = typeof link.cert.seq !== 'number'
159
+ if (legado && typeof link.cert.exp === 'number' && link.cert.exp <= Date.now()) {
160
+ return { renewed: false, reason: 'expired: enroll again' }
161
+ }
162
+ const atrasado = typeof link.actaSeq === 'number' && typeof link.cert.seq === 'number' && link.actaSeq > link.cert.seq
163
+ if (!force && !legado && !atrasado) return { renewed: false, seq: link.cert.seq, reason: 'not due' }
157
164
  installNodeGlobals(dir)
158
165
  try {
159
166
  const res = await requestRenew({ master: link.iss, proxy: link.proxy || 'wss://proxy.dotrino.com', device: link.device, cert: link.cert })
160
167
  const cert = res?.cert
161
- if (!cert || cert.sub !== link.device.publickey || cert.iss !== link.iss || !(cert.exp > exp)) {
162
- throw new Error('the vault returned a cert that is not for this machine')
163
- }
164
- const v = await verifyDelegation({ cert, expectedSub: link.device.publickey })
168
+ const acta = res?.acta
169
+ if (!cert || cert.sub !== link.device.publickey) throw new Error('the vault returned a cert that is not for this machine')
170
+ // El acta viaja con el papel y hace falta para juzgarlo: quien lo firmó tiene que ser
171
+ // SELLADORA de este perfil. Ya no se compara `cert.iss` con una llave fija — con varias
172
+ // bóvedas el emisor puede ser otra del mismo perfil; lo que se fija es el PERFIL.
173
+ if (!acta) throw new Error('the vault did not send its record: cannot check who signed this cert')
174
+ if (acta.profileId !== link.iss) throw new Error('the record is from a profile other than the pinned one')
175
+ const v = await verifyDelegation({ cert, expectedSub: link.device.publickey, actaSeq: acta.seq, sealers: sealersOf(acta) })
165
176
  if (!v.ok) throw new Error(v.reason)
177
+ // SE GUARDA ANTES DE DAR NADA POR BUENO. Emitir un papel RETIRA el anterior, así que si
178
+ // esto no se persiste la máquina se queda usando uno revocado y fuera para siempre. Le
179
+ // pasó al registro de selladores en la migración, y por eso está dicho aquí.
166
180
  link.cert = cert
181
+ link.acta = acta
182
+ link.actaSeq = acta.seq
167
183
  saveLink(dir, link)
168
- return { renewed: true, exp: cert.exp }
184
+ return { renewed: true, seq: cert.seq }
169
185
  } catch (e) {
170
- return { renewed: false, exp, reason: e.message }
186
+ return { renewed: false, seq: link.cert.seq ?? null, reason: e.message }
171
187
  }
172
188
  }
173
189