@dotrino/identity 0.40.0 → 0.42.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.40.0",
3
+ "version": "0.42.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
@@ -269,6 +269,8 @@ export class Identity {
269
269
 
270
270
  /** Dónde estoy yo: { inProfile, profileId, seq, isMaster, caps, id }. */
271
271
  async myMembership () { return this._call('myMembership') }
272
+ /** La cadena de actas desde `sinceSeq` (ver la nota en `node.js`). */
273
+ async actaHistory (opts) { return this._call('actaHistory', opts || {}) }
272
274
 
273
275
  /** ¿Es ESTE dispositivo el master (el único que puede cambiar el acta)? */
274
276
  async isMaster () { return this._call('isMaster') }
package/src/node.js CHANGED
@@ -165,6 +165,17 @@ export class Identity {
165
165
  profileActa () { return this._h('profileActa') }
166
166
  profileMembers () { return this._h('profileMembers') }
167
167
  myMembership () { return this._h('myMembership') }
168
+ /**
169
+ * La CADENA de actas desde `sinceSeq` (para que un dispositivo que estuvo apagado
170
+ * compruebe el encadenamiento en vez de tragarse un salto a ciegas).
171
+ *
172
+ * Vivía en el núcleo pero NO estaba aquí, así que `identity.actaHistory` era
173
+ * `undefined` en el daemon: al pedir la lista de dispositivos con un `sinceSeq`
174
+ * —que es lo que manda SIEMPRE un navegador ya emparejado— reventaba con
175
+ * «is not a function», la bóveda respondía error, y el aparato NUNCA volvía a
176
+ * sincronizar su acta. Se quedaba con la copia del día que emparejó.
177
+ */
178
+ actaHistory (opts) { return this._h('actaHistory', opts || {}) }
168
179
  isMaster () { return this._h('isMaster') }
169
180
  admitMember (member) { return this._h('admitMember', member) }
170
181
  setCaps (pub, caps) { return this._h('setCaps', { pub, caps }) }
package/vault/acta.js CHANGED
@@ -245,6 +245,15 @@ export async function applyChanges (acta, changes, { by, now = Date.now() } = {}
245
245
  // No se puede ascender un servicio a dispositivo cambiándole las capacidades: para
246
246
  // eso hay que sacarlo y volver a admitirlo, que es un gesto visible.
247
247
  m.caps = cleanCaps(ch.caps).filter((c) => (m.cn ? SERVICE_CAPS : DEVICE_CAPS).includes(c))
248
+ // CONCEDER LIMPIA LA RENUNCIA. `effectiveCaps` resta lo renunciado, así que sin
249
+ // esto una renuncia sellada era IRREVERSIBLE: el master le devolvía el permiso en
250
+ // `caps` y la resta seguía dejándolo fuera, para siempre. La interfaz promete
251
+ // «puedes devolvérselo desde el Master», así que tiene que ser verdad.
252
+ // Se tira el registro entero y no se le recortan capacidades: va FIRMADO por el
253
+ // miembro, y editarlo lo dejaría sin firma válida.
254
+ const renunciadas = new Set()
255
+ for (const r of next.renounced) if (r.member === m.pub) for (const c of (r.caps || [])) renunciadas.add(c)
256
+ if (m.caps.some((c) => renunciadas.has(c))) next.renounced = next.renounced.filter((r) => r.member !== m.pub)
248
257
  break
249
258
  }
250
259
  case 'label': {
package/vault/core.js CHANGED
@@ -22,7 +22,7 @@ import { signDelegationWith, MAX_DELEGATION_MS, DEFAULT_DELEGATION_MS } from './
22
22
  import * as Acta from './acta.js'
23
23
  import * as Content from './content.js'
24
24
  import { pubkeyId as pubkeyIdOf } from './capabilities.js'
25
- import { enrollDevice as remoteEnroll, requestSign as remoteSign, requestStore as remoteStore, requestDevices as remoteDevices, requestRenew as remoteRenew, requestAdmin as remoteAdmin } from './remote.js'
25
+ import { enrollDevice as remoteEnroll, requestSign as remoteSign, requestStore as remoteStore, requestDevices as remoteDevices, requestRenew as remoteRenew, requestAdmin as remoteAdmin, requestRenounce as remoteRenounce } from './remote.js'
26
26
 
27
27
  export const KEY_STORAGE = 'dotrino.identity.keypair'
28
28
  export const ENC_KEY_STORAGE = 'dotrino.identity.enc-keypair'
@@ -1270,6 +1270,16 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1270
1270
  pend.push(record)
1271
1271
  saveRenounces(pend)
1272
1272
  emitVault({ phase: 'renounced', caps: record.caps })
1273
+ // Y se le MANDA a la bóveda para que la selle en el acta. Sin esto la renuncia solo
1274
+ // valía aquí dentro: la bóveda seguía teniendo escrito que este aparato puede firmar
1275
+ // y le seguía aceptando peticiones. Best-effort: si la bóveda está apagada, la
1276
+ // renuncia ya está en pie localmente (para eso no necesita a nadie) y se reintenta
1277
+ // la próxima vez que se renuncie.
1278
+ const v = loadVaultCert(); const dev = loadVaultDevice()
1279
+ if (v?.cert && dev) {
1280
+ remoteRenounce({ master: v.master, proxy: v.proxy, device: dev, cert: v.cert, record })
1281
+ .catch(() => {})
1282
+ }
1273
1283
  // Si además soy el master, la absorbo ya en el acta.
1274
1284
  if (amMaster()) { try { await sealChanges([{ op: 'renounce', record }]) } catch (_) {} }
1275
1285
  return { ok: true, record, caps: Acta.effectiveCaps(loadActa(), publickeyJwkStr, loadRenounces()) }
package/vault/remote.js CHANGED
@@ -32,6 +32,9 @@ const MSG = {
32
32
  ADMIN: 'vault.admin',
33
33
  ADMIN_RESULT: 'vault.admin.result',
34
34
  ADMIN_EVENT: 'vault.admin.event',
35
+ // Renuncia: el miembro se quita capacidades a sí mismo y la bóveda la sella en el acta.
36
+ RENOUNCE: 'vault.renounce',
37
+ RENOUNCE_RESULT: 'vault.renounce.result',
35
38
  ERROR: 'vault.error'
36
39
  }
37
40
  export { MSG as VAULT_MSG }
@@ -275,6 +278,26 @@ export async function requestRenew ({ master, proxy, device, cert, onRevoked } =
275
278
  return { cert: res.cert }
276
279
  }
277
280
 
281
+ /**
282
+ * Manda a la bóveda la RENUNCIA de este dispositivo para que la selle en el acta.
283
+ *
284
+ * Sin esto, renunciar solo valía en el propio aparato: la bóveda seguía teniendo escrito
285
+ * que podía firmar, le seguía aceptando peticiones, y cualquiera que mirara el acta lo
286
+ * seguía viendo como firmante. Una renuncia que no llega al acta no es oponible a nadie —
287
+ * y en el caso que la justifica (te robaron el aparato) su registro local no vale nada,
288
+ * porque lo borra quien lo tenga en la mano.
289
+ *
290
+ * El `record` va FIRMADO por el propio miembro y solo puede QUITAR, así que la bóveda no
291
+ * necesita comprobar ningún certificado para honrarlo: le basta la firma.
292
+ */
293
+ export async function requestRenounce ({ master, proxy, device, cert, record, onRevoked } = {}) {
294
+ const res = await vaultRpc({
295
+ master, proxy, device, cert, onRevoked,
296
+ sendType: MSG.RENOUNCE, okType: MSG.RENOUNCE_RESULT, data: { op: 'renounce', record }
297
+ })
298
+ return { ok: !!res.ok, seq: res.seq ?? null }
299
+ }
300
+
278
301
  /**
279
302
  * CONSOLA REMOTA (`dotrino-vault/docs/consola-remota.md`): administrar el perfil desde
280
303
  * este dispositivo, sin ir al PC. Requiere un cert con scope `vault:admin`, que **no se