@dotrino/vaultd 0.62.1 → 0.63.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/lib/src/index.js CHANGED
@@ -26,7 +26,7 @@
26
26
  * No reimplementa nada del ecosistema.
27
27
  */
28
28
  import { verifyChain, verifyDeviceSig } from '@dotrino/identity/capabilities'
29
- import { memberCanSign } from '@dotrino/identity/acta'
29
+ import { memberCanScope } from '@dotrino/identity/acta'
30
30
  import { createEnrollDesk, deviceIdOf, DEVICE_TTL_MS, FRESH_WINDOW_MS } from './enroll.js'
31
31
  // Las constantes del protocolo salen del MISMO módulo que usa el daemon: si la lista
32
32
  // local se queda corta, el dispositivo deja de handle mensajes sin que nadie lo note.
@@ -195,6 +195,17 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
195
195
  send(from, { type: MSG.ERROR, error: 'unauthorized: ' + chk.reason })
196
196
  return null
197
197
  }
198
+ // EL ACTA MANDA, EL PAPEL SOLO ACOMPAÑA. Aquí y no en cada mostrador: el certificado
199
+ // dice a qué se comprometió esta bóveda al conectar el aparato, y el acta lo que puede
200
+ // HOY. Quitarle un permiso sella el acta pero no le retira el papel, que vive hasta 30
201
+ // días, así que sin esto seguiría entrando hasta que lo renovara.
202
+ if (expectedScope) {
203
+ const record = (await identity.profileActa?.().catch(() => null))?.acta || null
204
+ if (record && !memberCanScope(record, chk.device, expectedScope)) {
205
+ send(from, { type: MSG.ERROR, error: 'unauthorized: acta — this member no longer has that permission' })
206
+ return null
207
+ }
208
+ }
198
209
  return chk
199
210
  }
200
211
 
@@ -206,14 +217,6 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
206
217
  async function handleSign (from, p) {
207
218
  const chk = await authorise(from, p, SCOPE.SIGN)
208
219
  if (!chk) return
209
- // MANDA EL ACTA, NO EL PAPEL. `authorise` mira el certificado, y quitarle `firma` a un
210
- // aparato NO reemite ni revoca su cert: seguiría diciendo `vault:sign` hasta 30 días.
211
- // Sin esto, retirarle el permiso lo cambiaba de firmar él a que firmara la bóveda por
212
- // él — o sea, no se lo retiraba. Mismo arreglo que en el daemon (`src/vault.js`).
213
- const record = (await identity.profileActa?.().catch(() => null))?.acta || null
214
- if (record && !memberCanSign(record, chk.device)) {
215
- return send(from, { type: MSG.ERROR, error: 'unauthorized: acta — this member does not sign' })
216
- }
217
220
  const toSign = p.data?.payload
218
221
  if (toSign == null) return send(from, { type: MSG.ERROR, error: 'data.payload required' })
219
222
  const { signature, publickey } = await identity.signData(toSign)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vaultd",
3
- "version": "0.62.1",
3
+ "version": "0.63.0",
4
4
  "type": "module",
5
5
  "description": "Certificador personal de Dotrino: daemon headless que custodia la clave maestra y delega capacidades a tus dispositivos por el proxy. Tu CA propia.",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "node": ">=20"
21
21
  },
22
22
  "dependencies": {
23
- "@dotrino/identity": "^0.65.1",
23
+ "@dotrino/identity": "^0.66.0",
24
24
  "@dotrino/passmanager": "^0.9.0",
25
25
  "@dotrino/proxy-client": "^0.13.1",
26
26
  "ws": "^8.18.0"
package/src/vault.js CHANGED
@@ -307,6 +307,28 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
307
307
  }
308
308
  })
309
309
 
310
+ /**
311
+ * EL ACTA MANDA, EL PAPEL SOLO ACOMPAÑA. Se pregunta DESPUÉS de verificar la cadena y
312
+ * ANTES de hacer nada, en todos los mostradores.
313
+ *
314
+ * El certificado dice a qué se comprometió esta bóveda cuando conectó el aparato; el
315
+ * acta dice lo que puede HOY. Y no coinciden: `caps <ID> -lee` sella el acta pero no
316
+ * reemite ni revoca el papel, que vive hasta 30 días. Sin esto, quitarle un permiso a un
317
+ * aparato no se lo quitaba — seguía leyendo, guardando o firmando un mes más.
318
+ *
319
+ * Va por `memberCanScope` (el pilar) y no a mano en cada sitio: la regla es la misma en
320
+ * las dos bóvedas y se comprobaba mal en las dos.
321
+ *
322
+ * @returns {boolean} `true` si puede seguir; si no, ya contestó el «no».
323
+ */
324
+ async function actaAllows (from, chk, scope, what) {
325
+ const record = (await identity.profileActa?.().catch(() => null))?.acta || null
326
+ if (!record || Acta.memberCanScope(record, chk.device, scope)) return true
327
+ audit('rejected', { what, reason: 'acta' })
328
+ reply(from, { type: MSG.ERROR, error: 'unauthorized: acta — this member no longer has that permission' })
329
+ return false
330
+ }
331
+
310
332
  // --- handleSign / handleGet: idénticos (verifyChain de la cadena D←maestra) ---
311
333
  async function handleSign (from, p) {
312
334
  if (!isFresh(p.data)) { audit('rejected', { what: 'sign', reason: 'stale' }); return staleReply(from) }
@@ -323,11 +345,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
323
345
  // `signData` local veía que ya no puede y se lo PEDÍA a la bóveda… que decía que sí.
324
346
  // O sea que quitarle el permiso de firmar no se lo quitaba: lo cambiaba de firmar él
325
347
  // a que firmaras tú por él.
326
- const record = (await identity.profileActa?.().catch(() => null))?.acta || null
327
- if (record && !Acta.memberCanSign(record, chk.device)) {
328
- audit('rejected', { what: 'sign', reason: 'acta' })
329
- return reply(from, { type: MSG.ERROR, error: 'unauthorized: acta — this member does not sign' })
330
- }
348
+ if (!await actaAllows(from, chk, SCOPE.SIGN, 'sign')) return
331
349
 
332
350
  // Y CERRADA NO FIRMA NADA (dueño, 2026-08-31). La regla de antes —«el daemon sigue
333
351
  // firmando con el perfil bloqueado, para que un reinicio no deje las apps muertas»— es
@@ -355,6 +373,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
355
373
  expectedScope: SCOPE.READ, trustedIssuer: master, revoked: await revocationSet()
356
374
  })
357
375
  if (!chk.ok) return denyChain(from, chk, p, 'get')
376
+ if (!await actaAllows(from, chk, SCOPE.READ, 'get')) return
358
377
  const id = p.data?.id || 'root'
359
378
  reply(from, { type: MSG.DATA, id, node: store.getNode(id) })
360
379
  }
@@ -383,6 +402,10 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
383
402
  chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, expectedScope: SCOPE.READ, trustedIssuer: master, revoked })
384
403
  }
385
404
  if (!chk.ok) return denyChain(from, chk, p, 'store')
405
+ // El scope que valió es el que hay que preguntarle al acta: un método de lectura pudo
406
+ // pasar con `read` en vez de `store`, y exigirle `store` al acta lo rechazaría a pesar
407
+ // de tener permiso para lo que pidió.
408
+ if (!await actaAllows(from, chk, chk.scope === SCOPE.READ ? SCOPE.READ : SCOPE.STORE, 'store')) return
386
409
  try {
387
410
  // CIFRADO de punta a punta con la clave de contenido del perfil: el proxy transporta
388
411
  // pero no ve nada de lo que el usuario guarda. Si el dispositivo mandó `enc`, se abre