@dotrino/remote-agent 0.6.2 → 0.7.1

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/link.js +33 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/remote-agent",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
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/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
 
@@ -119,7 +120,13 @@ export function identityFromLink (link, { dir = dataDir() } = {}) {
119
120
  },
120
121
  async getVaultCert () { return { cert: link.cert } },
121
122
  async vaultStatus () {
122
- return { paired: true, master: link.iss, proxy, exp: link.cert.exp, deviceId: await pubkeyId(device.publickey) }
123
+ // `seq` es lo que describe un papel ahora; `exp` solo lo llevan los del modelo viejo
124
+ // y se sigue dando mientras existan, para que quien mire sepa que le falta migrar.
125
+ return {
126
+ paired: true, master: link.iss, proxy, deviceId: await pubkeyId(device.publickey),
127
+ seq: link.cert?.seq ?? null,
128
+ ...(typeof link.cert?.exp === 'number' ? { exp: link.cert.exp, legacy: true } : {})
129
+ }
123
130
  },
124
131
  async listVaultDevices () {
125
132
  // SIN `sinceSeq`: un agente no guarda el acta ni la encadena —no hay una sola línea
@@ -150,24 +157,39 @@ export const RENEW_BEFORE_MS = 7 * 24 * 60 * 60 * 1000
150
157
  * @returns {Promise<{ renewed: boolean, exp: number|null, reason?: string }>}
151
158
  */
152
159
  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' }
160
+ if (!link?.cert) return { renewed: false, seq: null, reason: 'no cert' }
161
+ // EL PAPEL DEL MODELO VIEJO SE CAMBIA SÍ O SÍ. Lleva `exp` y no `seq`; se acepta por el
162
+ // repliegue de migración pero muere en su fecha, y con él la máquina. Los demás se
163
+ // renuevan cuando el acta va por delante — nunca por calendario, que el papel no vence.
164
+ const legado = typeof link.cert.seq !== 'number'
165
+ if (legado && typeof link.cert.exp === 'number' && link.cert.exp <= Date.now()) {
166
+ return { renewed: false, reason: 'expired: enroll again' }
167
+ }
168
+ const atrasado = typeof link.actaSeq === 'number' && typeof link.cert.seq === 'number' && link.actaSeq > link.cert.seq
169
+ if (!force && !legado && !atrasado) return { renewed: false, seq: link.cert.seq, reason: 'not due' }
157
170
  installNodeGlobals(dir)
158
171
  try {
159
172
  const res = await requestRenew({ master: link.iss, proxy: link.proxy || 'wss://proxy.dotrino.com', device: link.device, cert: link.cert })
160
173
  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 })
174
+ const acta = res?.acta
175
+ if (!cert || cert.sub !== link.device.publickey) throw new Error('the vault returned a cert that is not for this machine')
176
+ // El acta viaja con el papel y hace falta para juzgarlo: quien lo firmó tiene que ser
177
+ // SELLADORA de este perfil. Ya no se compara `cert.iss` con una llave fija — con varias
178
+ // bóvedas el emisor puede ser otra del mismo perfil; lo que se fija es el PERFIL.
179
+ if (!acta) throw new Error('the vault did not send its record: cannot check who signed this cert')
180
+ if (acta.profileId !== link.iss) throw new Error('the record is from a profile other than the pinned one')
181
+ const v = await verifyDelegation({ cert, expectedSub: link.device.publickey, actaSeq: acta.seq, sealers: sealersOf(acta) })
165
182
  if (!v.ok) throw new Error(v.reason)
183
+ // SE GUARDA ANTES DE DAR NADA POR BUENO. Emitir un papel RETIRA el anterior, así que si
184
+ // esto no se persiste la máquina se queda usando uno revocado y fuera para siempre. Le
185
+ // pasó al registro de selladores en la migración, y por eso está dicho aquí.
166
186
  link.cert = cert
187
+ link.acta = acta
188
+ link.actaSeq = acta.seq
167
189
  saveLink(dir, link)
168
- return { renewed: true, exp: cert.exp }
190
+ return { renewed: true, seq: cert.seq }
169
191
  } catch (e) {
170
- return { renewed: false, exp, reason: e.message }
192
+ return { renewed: false, seq: link.cert.seq ?? null, reason: e.message }
171
193
  }
172
194
  }
173
195