@dotrino/vault 0.18.0 → 0.20.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/vault",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Usa ESTE dispositivo (navegador) como bóveda/CA del ecosistema Dotrino: atiende enrolamientos por el proxy y firma certificados de delegación a tus máquinas. Incluye el cliente de SERVICIO (Node): un proyecto se enrola una vez y jala sus credenciales del vault en vez del .env (`import '@dotrino/vault/config'`).",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/admin.js CHANGED
@@ -127,7 +127,15 @@ export function createAdminDesk ({
127
127
  return { ok: true, result: { ok: true } }
128
128
  }
129
129
 
130
+ // Quitar un dispositivo: por `sub` se le retiran TODOS sus certificados. Con
131
+ // `certNonce` a secas solo cae ese papel, y el aparato puede tener otro vigente.
130
132
  if (data.op === 'revoke') {
133
+ if (data.sub) {
134
+ const r = await desk.revokeDevice(String(data.sub))
135
+ audit('admin.revoke-device', { by, certs: r?.nonces?.length ?? null })
136
+ await notify('revoked', { by })
137
+ return { ok: true, result: r || { ok: true } }
138
+ }
131
139
  const r = await desk.revoke(String(data.certNonce || ''))
132
140
  audit('admin.revoke', { by, certNonce: data.certNonce })
133
141
  await notify('revoked', { certNonce: data.certNonce, by })
package/src/enroll.js CHANGED
@@ -441,9 +441,32 @@ export function createEnrollDesk ({
441
441
  return res
442
442
  }
443
443
 
444
+ /**
445
+ * QUITA EL DISPOSITIVO entero: retira TODOS sus certificados vigentes y le manda una
446
+ * sola orden de autoborrado. `revoke(nonce)` retira un papel, y un aparato puede tener
447
+ * varios (una renovación dejaba vivo el anterior): quitarle uno no lo echaba, y podía
448
+ * quedarse dentro justo con el que llevaba `vault:admin`.
449
+ */
450
+ async function revokeDevice (sub) {
451
+ if (!sub) throw new Error('sub (device pubkey) required')
452
+ const { issued } = await identity.listDelegations()
453
+ const mine = (issued || []).filter((d) => d.sub === sub)
454
+ audit('revoke-device', { certs: mine.length })
455
+ // Si el núcleo no trae `revokeDevice` (bóveda vieja), se cae a retirarlos uno a uno.
456
+ const res = identity.revokeDevice
457
+ ? await identity.revokeDevice(sub)
458
+ : { ok: true, nonces: await (async () => {
459
+ const done = []
460
+ for (const d of mine) { await identity.revokeDelegation(d.nonce); done.push(d.nonce) }
461
+ return done
462
+ })() }
463
+ await emitRevoke(sub, mine[0]?.nonce || null)
464
+ return res
465
+ }
466
+
444
467
  return {
445
468
  startPairing, stopPairing, handleEnroll, handleActaSealed, handleHello, approve, reject,
446
- listPending, findPending, emitRevoke, revoke,
469
+ listPending, findPending, emitRevoke, revoke, revokeDevice,
447
470
  get pendingCount () { return pending.size }
448
471
  }
449
472
  }
package/src/env.js CHANGED
@@ -172,7 +172,7 @@ export function applyEnv (secrets, override = overrideByDefault()) {
172
172
  * @param {Object} [opts]
173
173
  * @param {string} [opts.ns]
174
174
  * @param {string} [opts.dir]
175
- * @param {(info:{ns:string, ts:number, motivo:'cambio'|'revocado'})=>void} [opts.onUpdate]
175
+ * @param {(info:{ns:string, ts:number, reason:'changed'|'revoked'})=>void} [opts.onUpdate]
176
176
  * Reemplaza la salida por defecto. Úsalo cuando terminar el proceso no sea una
177
177
  * opción — el caso del proxio, cuyo reinicio corta el transporte de todos.
178
178
  * @param {number} [opts.exitCode=0] Salida LIMPIA: systemd con `Restart=on-failure`
@@ -183,24 +183,24 @@ export function applyEnv (secrets, override = overrideByDefault()) {
183
183
  export async function watchEnv ({ ns, dir, onUpdate, exitCode = 0, quiet = false, ...resto } = {}) {
184
184
  ns = resolveNs(ns)
185
185
  dir = dir || serviceDir(ns)
186
- const decir = (m) => { if (!quiet) console.error(m) }
186
+ const say = (m) => { if (!quiet) console.error(m) }
187
187
 
188
- const salir = (motivo) => {
189
- decir(`[dotrino-env] ${motivo === 'revocado'
188
+ const exitNow = (reason) => {
189
+ say(`[dotrino-env] ${reason === 'revoked'
190
190
  ? 'la bóveda REVOCÓ este agente: terminando (no volverá a arrancar)'
191
191
  : 'configuración nueva en la bóveda: terminando para que el supervisor lo levante limpio'}`)
192
- process.exit(motivo === 'revocado' ? 1 : exitCode)
192
+ process.exit(reason === 'revoked' ? 1 : exitCode)
193
193
  }
194
194
 
195
195
  return watchSecretsChanges({
196
196
  dir,
197
197
  ns,
198
- log: decir,
199
- onChange: ({ ts }) => (onUpdate ? onUpdate({ ns, ts, motivo: 'cambio' }) : salir('cambio')),
198
+ log: say,
199
+ onChange: ({ ts }) => (onUpdate ? onUpdate({ ns, ts, reason: 'changed' }) : exitNow('changed')),
200
200
  // Un cert revocado sale con código de FALLO a propósito: si el supervisor lo
201
201
  // levanta, va a morir otra vez al no poder leer sus secretos, y el contador de
202
202
  // reinicios fallidos es lo que hace que se note en vez de girar en silencio.
203
- onRevoked: () => (onUpdate ? onUpdate({ ns, ts: Date.now(), motivo: 'revocado' }) : salir('revocado')),
203
+ onRevoked: () => (onUpdate ? onUpdate({ ns, ts: Date.now(), reason: 'revoked' }) : exitNow('revoked')),
204
204
  ...resto
205
205
  })
206
206
  }
package/src/index.js CHANGED
@@ -155,14 +155,16 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
155
155
  if (typeof d.ts !== 'number' || Math.abs(Date.now() - d.ts) > FRESH_WINDOW_MS) return
156
156
  const chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, trustedIssuer: iss })
157
157
  if (!chk.ok) return send(from, { type: MSG.ERROR, error: 'no autorizado: ' + chk.reason })
158
- const { issued, revoked } = await identity.listDelegations()
158
+ const { issued, revoked, revokedCerts } = await identity.listDelegations()
159
159
  const devices = await Promise.all((issued || []).map(async (x) => ({
160
160
  deviceId: x.sub ? await deviceIdOf(x.sub) : null, sub: x.sub || null,
161
161
  label: x.label || '', scope: x.scope, exp: x.exp, nonce: x.nonce
162
162
  })))
163
163
  send(from, { type: MSG.DEVICES_RESULT, devices, revoked: (revoked || []).map((r) => r.nonce || r) })
164
164
  // ¿el que consulta es una máquina revocada que reapareció? → re-emite el REVOKED firmado.
165
- const mine = (issued || []).find((x) => x.sub === chk.device && x.revokedAt)
165
+ // Se busca en `revokedCerts`: desde que `issued` solo trae lo vigente, los retirados
166
+ // ya no están ahí y este aviso dejaba de dispararse (se caía en silencio).
167
+ const mine = (revokedCerts || issued || []).find((x) => x.sub === chk.device && x.revokedAt)
166
168
  if (mine) desk.emitRevoke(chk.device, mine.nonce)
167
169
  }
168
170
 
package/src/protocol.js CHANGED
@@ -42,6 +42,8 @@ export const MSG = Object.freeze({
42
42
  DEVICES: 'vault.devices', // dispositivo → vault: { data:{publickey,ts}, signature, cert }
43
43
  DEVICES_RESULT: 'vault.devices.result', // vault → dispositivo: { devices, revoked }
44
44
  RENEW: 'vault.renew', // dispositivo → vault: { data:{op,publickey,ts}, signature, cert }
45
+ RENOUNCE: 'vault.renounce', // dispositivo → vault: { record } (RENUNCIA firmada por el propio miembro)
46
+ RENOUNCE_RESULT: 'vault.renounce.result', // vault → dispositivo: { ok, seq }
45
47
  RENEWED: 'vault.renewed', // vault → dispositivo: { cert } (cert fresco, misma sub-clave/scope)
46
48
  SECRETS: 'vault.secrets', // servicio → vault: { data:{op,ns,ek,publickey,ts}, signature, cert }
47
49
  SECRETS_RESULT: 'vault.secrets.result', // vault → servicio: { body:{op,ns,enc,ts}, signature } (enc SELLADO a ek; body firmado por la maestra)