@dotrino/identity 0.42.0 → 0.44.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/identity",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "description": "Identidad y rating de usuarios compartidos entre apps de Dotrino (vault iframe + postMessage)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -251,6 +251,14 @@ export class Identity {
251
251
  return this._call('revokeDelegation', { nonce })
252
252
  }
253
253
 
254
+ /**
255
+ * QUITA EL DISPOSITIVO: retira **todos** los certificados vigentes de esa llave.
256
+ * Revocar por `nonce` retira un papel; un aparato puede tener otros y seguir entrando.
257
+ */
258
+ async revokeDevice (sub) {
259
+ return this._call('revokeDevice', { sub })
260
+ }
261
+
254
262
  /** Lista las delegaciones emitidas + la lista de revocación (para el gestor de dispositivos). */
255
263
  async listDelegations () {
256
264
  return this._call('listDelegations')
package/src/node.js CHANGED
@@ -160,6 +160,8 @@ export class Identity {
160
160
  // Delegación de capacidad (sub-clave de dispositivo con scope/exp/revocación)
161
161
  signDelegation (sub, scope, opts = {}) { return this._h('signDelegation', { sub, scope, ...opts }) }
162
162
  revokeDelegation (nonce) { return this._h('revokeDelegation', { nonce }) }
163
+ /** Quitar el DISPOSITIVO: retira todos sus certificados vigentes, no solo uno. */
164
+ revokeDevice (sub) { return this._h('revokeDevice', { sub }) }
163
165
  listDelegations () { return this._h('listDelegations') }
164
166
  // Acta de perfil (qué llaves son del perfil y qué puede cada una; ver acta-de-perfil.md)
165
167
  profileActa () { return this._h('profileActa') }
package/vault/core.js CHANGED
@@ -314,6 +314,27 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
314
314
  }
315
315
  const saveRevocations = (o) => kv.setItem(REVOCATIONS_STORAGE, JSON.stringify(o))
316
316
 
317
+ /**
318
+ * Retira todos los certificados vigentes de la llave `sub`, menos `keepNonce` (el recién
319
+ * emitido, cuando esto se llama desde `signDelegation`). Devuelve los nonces retirados.
320
+ * Solo toca las listas locales: avisar al aparato es cosa del mostrador de enrolamiento,
321
+ * que es quien puede firmar la orden de autoborrado.
322
+ */
323
+ function revokePriorCertsFor (sub, keepNonce) {
324
+ const store = loadDelegations()
325
+ const rev = loadRevocations()
326
+ const now = Date.now()
327
+ const hit = []
328
+ for (const [nonce, d] of Object.entries(store)) {
329
+ if (nonce === keepNonce || d?.sub !== sub || d?.revokedAt) continue
330
+ rev[nonce] = now
331
+ d.revokedAt = now
332
+ hit.push(nonce)
333
+ }
334
+ if (hit.length) { saveRevocations(rev); saveDelegations(store) }
335
+ return hit
336
+ }
337
+
317
338
  // ----- emparejamiento con el vault del usuario (este dispositivo enrolado) -----
318
339
  // Canal de eventos 'vault' (p.ej. el código a tipear durante el emparejamiento).
319
340
  const vaultListeners = new Set()
@@ -326,6 +347,13 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
326
347
  */
327
348
  const wipeVaultLink = () => {
328
349
  try { kv.removeItem(VAULT_CERT_STORAGE); kv.removeItem(VAULT_DEVICE_STORAGE) } catch (_) {}
350
+ // Y el ACTA. Sin esto el aparato borraba su enlace con la bóveda pero seguía
351
+ // enseñando el perfil del que acababan de echarlo —con sus miembros y sus permisos—,
352
+ // porque la copia local se quedaba ahí y ya no podía refrescarla (está revocado). El
353
+ // dueño lo describió exacto: «el device ni por enterado de que está fuera».
354
+ // Queda como lo que es: un dispositivo sin cuenta, que puede crear la suya o
355
+ // emparejarse otra vez.
356
+ try { kv.removeItem(ACTA_STORAGE) } catch (_) {}
329
357
  emitVault({ phase: 'revoked' })
330
358
  }
331
359
 
@@ -1043,7 +1071,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1043
1071
  // de dispositivo `sub`, acotado por `scope` y `exp`, revocable por `nonce`.
1044
1072
  // Es la ÚNICA forma en que la autoridad sale de la clave maestra, y va limitada.
1045
1073
 
1046
- async signDelegation ({ sub, scope, ttlMs, exp, nonce, label }) {
1074
+ async signDelegation ({ sub, scope, ttlMs, exp, nonce, label, supersede }) {
1047
1075
  if (!sub || typeof sub !== 'string') throw new Error('sub (device pubkey) required')
1048
1076
  if (!scope || (typeof scope !== 'string' && !Array.isArray(scope))) throw new Error('scope required')
1049
1077
  const iat = Date.now()
@@ -1054,9 +1082,27 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1054
1082
  const store = loadDelegations()
1055
1083
  store[cert.nonce] = { nonce: cert.nonce, sub, scope, iat, exp: cappedExp, label: typeof label === 'string' ? label.slice(0, 60) : '' }
1056
1084
  saveDelegations(store)
1085
+ // UNA LLAVE, UN CERTIFICADO VIGENTE. Renovar emitía uno nuevo y dejaba vivo el
1086
+ // anterior: el mismo aparato salía dos veces en la lista (parecían dos máquinas) y,
1087
+ // peor, «quitar el dispositivo» revocaba UN cert y el aparato seguía entrando con el
1088
+ // otro — a veces justo con el que llevaba `vault:admin`. Al firmar, los certs previos
1089
+ // de esa misma `sub` se retiran. Silencioso a propósito: NO se emite el aviso de
1090
+ // autoborrado (eso solo lo dispara una revocación de verdad, desde el mostrador).
1091
+ if (supersede !== false) revokePriorCertsFor(sub, cert.nonce)
1057
1092
  return { cert }
1058
1093
  },
1059
1094
 
1095
+ /**
1096
+ * Retira TODOS los certificados vigentes de un dispositivo (por su llave `sub`).
1097
+ * Es lo que significa «quitar el dispositivo»: revocar por `nonce` retira un papel,
1098
+ * no al aparato, que puede tener otros.
1099
+ */
1100
+ async revokeDevice ({ sub }) {
1101
+ if (!sub || typeof sub !== 'string') throw new Error('sub (device pubkey) required')
1102
+ const nonces = revokePriorCertsFor(sub, null)
1103
+ return { ok: true, nonces, revokedAt: Date.now() }
1104
+ },
1105
+
1060
1106
  async revokeDelegation ({ nonce }) {
1061
1107
  if (!nonce || typeof nonce !== 'string') throw new Error('nonce required')
1062
1108
  const rev = loadRevocations()
@@ -1067,10 +1113,16 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1067
1113
  return { ok: true, revokedAt: rev[nonce] }
1068
1114
  },
1069
1115
 
1116
+ // `issued` = lo que HOY sirve para entrar. Antes devolvía el almacén entero, revocados
1117
+ // incluidos (revocar solo estampa `revokedAt`), así que la consola seguía pintando como
1118
+ // activo un cert ya retirado: pulsabas «quitar» y la fila no se movía. Los caducados ya
1119
+ // los poda `loadDelegations`. El histórico retirado va aparte, en `revokedCerts`.
1070
1120
  async listDelegations () {
1071
1121
  const store = loadDelegations(); const rev = loadRevocations()
1122
+ const all = Object.values(store).sort((a, b) => (b.iat || 0) - (a.iat || 0))
1072
1123
  return {
1073
- issued: Object.values(store).sort((a, b) => (b.iat || 0) - (a.iat || 0)),
1124
+ issued: all.filter((d) => !d.revokedAt && !rev[d.nonce]),
1125
+ revokedCerts: all.filter((d) => d.revokedAt || rev[d.nonce]),
1074
1126
  revoked: Object.keys(rev).map(nonce => ({ nonce, revokedAt: rev[nonce] }))
1075
1127
  }
1076
1128
  },
package/vault/vault.js CHANGED
@@ -118,6 +118,7 @@ import { pubkeyId } from './capabilities.js'
118
118
  signDelegation: (sub, scope, opts) => handlers.signDelegation({ sub, scope, ...(opts || {}) }),
119
119
  listDelegations: () => handlers.listDelegations({}),
120
120
  revokeDelegation: (nonce) => handlers.revokeDelegation({ nonce }),
121
+ revokeDevice: (sub) => handlers.revokeDevice({ sub }),
121
122
  admitMember: (m) => handlers.admitMember(m),
122
123
  profileActa: () => handlers.profileActa({}),
123
124
  // Camino A (`mode: 'adopt'`): la bóveda se queda con la cuenta que trae el aparato,