@dotrino/identity 0.38.0 → 0.40.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.38.0",
3
+ "version": "0.40.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
@@ -278,6 +278,7 @@ export class Identity {
278
278
 
279
279
  /** Cambia las capacidades de un miembro (solo el master). */
280
280
  async setCaps (pub, caps) { return this._call('setCaps', { pub, caps }) }
281
+ async setLabel (pub, label) { return this._call('setLabel', { pub, label }) }
281
282
 
282
283
  /** Expulsa a un miembro (solo el master; al master no se le puede expulsar). */
283
284
  async removeMember (pub) { return this._call('removeMember', { pub }) }
package/src/node.js CHANGED
@@ -168,6 +168,7 @@ export class Identity {
168
168
  isMaster () { return this._h('isMaster') }
169
169
  admitMember (member) { return this._h('admitMember', member) }
170
170
  setCaps (pub, caps) { return this._h('setCaps', { pub, caps }) }
171
+ setLabel (pub, label) { return this._h('setLabel', { pub, label }) }
171
172
  removeMember (pub) { return this._h('removeMember', { pub }) }
172
173
  handoverMaster (to, member = null) { return this._h('handoverMaster', { to, member }) }
173
174
  renounceCaps (caps) { return this._h('renounceCaps', { caps }) }
package/vault/acta.js CHANGED
@@ -247,6 +247,17 @@ export async function applyChanges (acta, changes, { by, now = Date.now() } = {}
247
247
  m.caps = cleanCaps(ch.caps).filter((c) => (m.cn ? SERVICE_CAPS : DEVICE_CAPS).includes(c))
248
248
  break
249
249
  }
250
+ case 'label': {
251
+ // RENOMBRAR un miembro. La etiqueta se escribía solo al admitir, con lo que el
252
+ // aparato se quedaba para siempre con el nombre que tuviera el día que entró
253
+ // (normalmente el apodo del usuario en ese momento), y para cambiarlo había que
254
+ // revocarlo y volver a emparejarlo. Es un nombre para el humano: no toca permisos
255
+ // ni llaves, pero se sella y se firma como cualquier otro cambio del acta.
256
+ const m = find(ch.pub)
257
+ if (!m) throw new Error('label: that member is not in the record')
258
+ m.label = String(ch.label || '').slice(0, 60)
259
+ break
260
+ }
250
261
  case 'remove': {
251
262
  const i = next.members.findIndex((m) => m.pub === ch.pub)
252
263
  if (i < 0) throw new Error('remove: that member is not in the record')
package/vault/core.js CHANGED
@@ -543,21 +543,49 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
543
543
  const RENEW_WINDOW_MS = 15 * 24 * 60 * 60 * 1000
544
544
  const RENEW_RETRY_MS = 60 * 60 * 1000 // si falla (vault apagado), no insistir >1 vez/hora
545
545
  let renewLastTry = 0
546
+ /**
547
+ * ¿El cert se quedó atrás respecto del ACTA? El acta es la política (lo que el dueño
548
+ * decidió); el cert es su reflejo, y solo se refresca al renovar. Sin esta comprobación,
549
+ * un permiso concedido después de emparejar tardaba en llegar lo que tardara el cert en
550
+ * acercarse a su caducidad: hasta 30 días. En la práctica, dar «administra» y no ver
551
+ * NUNCA aparecer la consola remota.
552
+ */
553
+ function certDesfasadoDelActa () {
554
+ try {
555
+ const v = loadVaultCert(); const acta = loadActa()
556
+ if (!v?.cert || !acta) return false
557
+ const debeTener = Acta.memberScopes(acta, publickeyJwkStr)
558
+ if (!debeTener.length) return false // ya no soy miembro: renovar no toca
559
+ const tiene = new Set(v.cert.scope || [])
560
+ return debeTener.length !== tiene.size || debeTener.some((s) => !tiene.has(s))
561
+ } catch (_) { return false }
562
+ }
563
+
546
564
  function maybeRenewVaultCert () {
547
565
  try {
548
566
  const v = loadVaultCert(); const device = loadVaultDevice()
549
567
  if (!v?.cert || !device) return
550
568
  const now = Date.now()
551
- if (v.cert.exp <= now || v.cert.exp - now > RENEW_WINDOW_MS) return
569
+ // Se renueva por dos motivos: porque el cert se acerca a su fin, o porque el acta
570
+ // dice que este aparato puede algo distinto de lo que lleva escrito el cert.
571
+ const porCaducar = v.cert.exp > now && v.cert.exp - now <= RENEW_WINDOW_MS
572
+ if (v.cert.exp <= now || (!porCaducar && !certDesfasadoDelActa())) return
552
573
  if (now - renewLastTry < RENEW_RETRY_MS) return
553
- renewLastTry = now
554
- remoteRenew({ master: v.master, proxy: v.proxy, device, cert: v.cert, onRevoked: wipeVaultLink }).then(({ cert }) => {
555
- kv.setItem(VAULT_CERT_STORAGE, JSON.stringify({ ...v, cert, renewedAt: Date.now() }))
556
- emitVault({ phase: 'renewed', exp: cert.exp })
557
- }).catch(() => {}) // best-effort: el cert vigente sigue sirviendo mientras tanto
574
+ renovarCert().catch(() => {}) // best-effort: el cert vigente sigue sirviendo mientras tanto
558
575
  } catch (_) {}
559
576
  }
560
577
 
578
+ /** Pide un cert fresco y lo guarda. Devuelve promesa para poder ESPERARLO cuando hace falta. */
579
+ async function renovarCert () {
580
+ const v = loadVaultCert(); const device = loadVaultDevice()
581
+ if (!v?.cert || !device) return null
582
+ renewLastTry = Date.now()
583
+ const { cert } = await remoteRenew({ master: v.master, proxy: v.proxy, device, cert: v.cert, onRevoked: wipeVaultLink })
584
+ kv.setItem(VAULT_CERT_STORAGE, JSON.stringify({ ...v, cert, renewedAt: Date.now() }))
585
+ emitVault({ phase: 'renewed', exp: cert.exp })
586
+ return cert
587
+ }
588
+
561
589
  // ----- me (kv-backed) -----
562
590
 
563
591
  function loadMe () {
@@ -1191,6 +1219,22 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1191
1219
  return { ok: true, seq: acta.seq }
1192
1220
  },
1193
1221
 
1222
+ /**
1223
+ * RENOMBRA un miembro (el nombre con el que lo reconoces). Se escribe en el acta y
1224
+ * TAMBIÉN en la delegación: son dos registros distintos —el acta dice quién es del
1225
+ * perfil, las delegaciones qué certs se emitieron— y las listas de dispositivos leen
1226
+ * la segunda, así que tocar solo una deja el nombre viejo a la vista.
1227
+ */
1228
+ async setLabel ({ pub, label } = {}) {
1229
+ const limpio = String(label || '').slice(0, 60)
1230
+ const acta = await sealChanges([{ op: 'label', pub, label: limpio }])
1231
+ const store = loadDelegations()
1232
+ let tocadas = 0
1233
+ for (const d of Object.values(store)) { if (d.sub === pub) { d.label = limpio; tocadas++ } }
1234
+ if (tocadas) saveDelegations(store)
1235
+ return { ok: true, seq: acta.seq, label: limpio, delegations: tocadas }
1236
+ },
1237
+
1194
1238
  async removeMember ({ pub } = {}) {
1195
1239
  const acta = await sealChanges([{ op: 'remove', pub }])
1196
1240
  // Expulsar rota la clave: el que sale no podrá abrir el contenido NUEVO. Lo que ya
@@ -1512,7 +1556,12 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1512
1556
  * cert que le dio la bóveda, no de una preferencia: la interfaz pregunta para
1513
1557
  * saber qué pintar, pero quien decide es la bóveda al recibir la petición.
1514
1558
  */
1515
- canAdminVault () {
1559
+ async canAdminVault () {
1560
+ // Si el acta ya dice que este aparato administra pero el cert todavía no, se ESPERA a
1561
+ // renovarlo aquí mismo. Disparar la renovación y contestar «no» dejaba la consola
1562
+ // escondida hasta la siguiente visita, y el dueño —que acababa de dar el permiso— no
1563
+ // tenía forma de saber que solo faltaba recargar.
1564
+ if (certDesfasadoDelActa()) { try { await renovarCert() } catch (_) {} }
1516
1565
  const v = loadVaultCert()
1517
1566
  return !!v?.cert && (v.cert.scope || []).includes('vault:admin')
1518
1567
  },
@@ -1530,6 +1579,10 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1530
1579
  if (res.chain?.length && res.chain[0].profileId === loadActa()?.profileId) await adoptChain(res.chain)
1531
1580
  else if (res.acta) await (res.acta.profileId === loadActa()?.profileId ? adoptActa(res.acta) : joinProfile(res.acta))
1532
1581
  } catch (_) {}
1582
+ // El acta acaba de llegar: si trae permisos que el cert no lleva, se renueva YA. La
1583
+ // comprobación de arriba corrió ANTES de tenerla, así que sin esto haría falta una
1584
+ // segunda visita para estrenar un permiso recién concedido.
1585
+ maybeRenewVaultCert()
1533
1586
  return res
1534
1587
  } catch (e) { return handleVaultError(e) }
1535
1588
  },