@dotrino/identity 0.66.0 → 0.67.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.66.0",
3
+ "version": "0.67.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/vault/acta.js CHANGED
@@ -37,7 +37,7 @@
37
37
  import { canonicalStringify } from './core.js'
38
38
  import { signWithDevice, verifyDeviceSig, pubkeyId } from './capabilities.js'
39
39
 
40
- export const ACTA_V = 2
40
+ export const ACTA_V = 3
41
41
 
42
42
  /**
43
43
  * Versiones de acta que se ACEPTAN al leer. La 1 sigue entrando porque una v1 en el disco
@@ -46,7 +46,22 @@ export const ACTA_V = 2
46
46
  * asciende sola: el acta siguiente que selle la maestra ya sale v2 (ver `applyChanges`).
47
47
  * La única diferencia es la llave de sellado (§8.9), que en una v1 simplemente no hay.
48
48
  */
49
- const ACTA_LEIBLES = Object.freeze([1, 2])
49
+ /**
50
+ * v3 quita `acta.sealer` (dueño, 2026-08-31: «SEAL es el nuevo master»): sellar es un
51
+ * permiso y nada más.
52
+ *
53
+ * Las anteriores SE SIGUEN LEYENDO, y no por nostalgia: dejar de hacerlo dejaría sin abrir
54
+ * TODAS las cuentas que existen hoy —cada una tendría que rehacerse aparato por aparato—
55
+ * cuando lo único que hace falta es traducir el campo a un permiso al leerlas. Un acta
56
+ * vieja no se puede reescribir (está firmada), así que se lee tal cual y `canSeal` la
57
+ * entiende; la PRIMERA vez que esa cuenta cambie algo, la nueva sale ya en v3 y el campo
58
+ * desaparece solo.
59
+ */
60
+ const ACTA_LEIBLES = Object.freeze([1, 2, 3])
61
+ /** Desde esta versión, sellar es solo un permiso y no hay campo `sealer`. */
62
+ const V_SIN_CAMPO_SELLADOR = 3
63
+ /** ¿Este acta es de las viejas, con el campo? */
64
+ const conCampoSellador = (acta) => Number(acta?.v) < V_SIN_CAMPO_SELLADOR
50
65
 
51
66
  /**
52
67
  * Lista CERRADA de capacidades.
@@ -182,26 +197,20 @@ export async function memberId (pub) {
182
197
  */
183
198
  export const sealersOf = (acta, renounces = []) => {
184
199
  if (!acta) return []
185
- const extra = (acta.members || [])
186
- .filter((m) => m.pub !== acta.sealer && memberCan(acta, m.pub, 'sealer', renounces))
200
+ const porPermiso = (acta.members || [])
201
+ .filter((m) => memberCan(acta, m.pub, 'sealer', renounces))
187
202
  .map((m) => m.pub)
188
- return [acta.sealer, ...extra]
203
+ // Acta vieja: el que estaba en el campo sellaba SIN tener el permiso. Se traduce al
204
+ // leerla, que es la única forma —está firmada y no se puede reescribir—.
205
+ if (conCampoSellador(acta) && acta.sealer && !porPermiso.includes(acta.sealer)) {
206
+ return [acta.sealer, ...porPermiso]
207
+ }
208
+ return porPermiso
189
209
  }
190
210
 
191
211
  /** ¿Puede `pub` sellar la siguiente acta de este perfil? */
192
212
  export const canSeal = (acta, pub, renounces = []) =>
193
- !!acta && (pub === acta.sealer || memberCan(acta, pub, 'sealer', renounces))
194
-
195
- /**
196
- * El acta la firmó alguien DISTINTO del sellador que ella misma nombra.
197
- *
198
- * ⚠️ Desde el multivault esto ya NO significa «es un traspaso», y confundirlo costó un
199
- * desempate mal resuelto: cuando sella la segunda bóveda, el acta sigue nombrando
200
- * sellador a la primera y la firma la segunda, así que esto da `true` sin que nadie
201
- * traspase nada. Es condición NECESARIA de un traspaso, no suficiente — lo que lo
202
- * distingue es que un traspaso CAMBIA a quién nombra (ver `canAdopt`).
203
- */
204
- export const isHandover = (acta) => !!acta && acta.sealer !== acta.sealedBy
213
+ !!acta && (memberCan(acta, pub, 'sealer', renounces) || (conCampoSellador(acta) && pub === acta.sealer))
205
214
 
206
215
  /**
207
216
  * Acta de génesis: un perfil recién nacido tiene UN miembro (esta llave), que además es el
@@ -213,11 +222,10 @@ export function genesisActa ({ pub, encPub = null, sealPub = null, label = '', n
213
222
  return {
214
223
  v: ACTA_V,
215
224
  profileId: pub,
216
- sealer: pub,
217
225
  sealedBy: pub,
218
226
  seq: 1,
219
227
  prev: null,
220
- members: [{ pub, encPub, label: String(label || '').slice(0, 60), cn: null, caps: [...PAIRED_CAPS], addedAt: now, cert: null }],
228
+ members: [{ pub, encPub, label: String(label || '').slice(0, 60), cn: null, caps: [...PAIRED_CAPS, 'sealer'], addedAt: now, cert: null }],
221
229
  revoked: [],
222
230
  renounced: [],
223
231
  // Llavero del contenido: una entrada por generación, con la clave del perfil ENVUELTA
@@ -239,7 +247,10 @@ export function genesisActa ({ pub, encPub = null, sealPub = null, label = '', n
239
247
  export function checkShape (acta) {
240
248
  if (!acta || typeof acta !== 'object') return 'no-acta'
241
249
  if (!ACTA_LEIBLES.includes(acta.v)) return 'version'
242
- if (!isPub(acta.profileId) || !isPub(acta.sealer) || !isPub(acta.sealedBy)) return 'shape'
250
+ if (!isPub(acta.profileId) || !isPub(acta.sealedBy)) return 'shape'
251
+ // El campo solo existe —y solo se exige— en las viejas.
252
+ if (conCampoSellador(acta) && !isPub(acta.sealer)) return 'shape'
253
+ if (!conCampoSellador(acta) && acta.sealer != null) return 'sealer-no-va-en-v3'
243
254
  if (!Number.isInteger(acta.seq) || acta.seq < 1) return 'seq'
244
255
  if (acta.seq > 1 && typeof acta.prev !== 'string') return 'prev'
245
256
  if (!Array.isArray(acta.members) || acta.members.length === 0) return 'members'
@@ -279,7 +290,10 @@ export function checkShape (acta) {
279
290
  }
280
291
  }
281
292
  if (new Set(acta.members.map((m) => m.pub)).size !== acta.members.length) return 'miembro-duplicado'
282
- if (!acta.members.some((m) => m.pub === acta.sealer)) return 'sealer-no-es-miembro'
293
+ // SIN NADIE QUE SELLE, el acta está muerta: no habría forma de volver a cambiarla nunca.
294
+ // Es el hermano del cierre de `sign`, y sustituye a «el sellador tiene que ser miembro»
295
+ // ahora que sellar es solo un permiso y los permisos ya son de miembros por definición.
296
+ if (!sealersOf(acta).length) return 'sin-sellador'
283
297
  if (!acta.members.some((m) => m.caps.includes('sign'))) return 'sin-firmante'
284
298
  return null
285
299
  }
@@ -436,7 +450,6 @@ export async function applyChanges (acta, changes, { by, now = Date.now(), sealP
436
450
  case 'remove': {
437
451
  const i = next.members.findIndex((m) => m.pub === ch.pub)
438
452
  if (i < 0) throw new Error('remove: that member is not in the record')
439
- if (next.members[i].pub === next.sealer) throw new Error('remove: cannot remove the master; hand the sealing over first')
440
453
  // Quitar al miembro se lleva su permiso de sellar: no hay lista aparte que limpiar.
441
454
  const fuera = next.members.splice(i, 1)[0]
442
455
  // Sus envolturas se van con él: sin ellas no puede abrir ninguna generación. (El
@@ -447,11 +460,6 @@ export async function applyChanges (acta, changes, { by, now = Date.now(), sealP
447
460
  })
448
461
  break
449
462
  }
450
- case 'handover': {
451
- if (!find(ch.to)) throw new Error('handover: the new master must be a member (admit them in the same change)')
452
- next.sealer = ch.to
453
- break
454
- }
455
463
  case 'keyring': {
456
464
  // Generación NUEVA de la clave de contenido (al rotar: expulsar a alguien).
457
465
  const g = ch.generation
@@ -493,8 +501,31 @@ export async function applyChanges (acta, changes, { by, now = Date.now(), sealP
493
501
  if (!next.members.some((m) => m.caps.includes('sign'))) {
494
502
  throw new Error('the change would leave the profile with no member able to sign')
495
503
  }
496
- if (!next.members.some((m) => m.pub === next.sealer)) {
497
- throw new Error('the change would leave the record with no master')
504
+ // ASCENDER DE UNA VIEJA: el que sellaba por el CAMPO pasa a sellar por el PERMISO. Es
505
+ // toda la migración, y va aquí porque este es el momento en que la cuenta escribe su
506
+ // primera v3 — el acta vieja no se puede tocar, está firmada.
507
+ if (conCampoSellador(acta) && acta.sealer) {
508
+ const m = next.members.find((x) => x.pub === acta.sealer)
509
+ if (m && !m.caps.includes('sealer')) m.caps = cleanCaps([...m.caps, 'sealer'])
510
+ // Y el campo se va con la vieja: la nueva es v3 y en v3 no existe.
511
+ delete next.sealer
512
+ }
513
+ if (!sealersOf(next).length) {
514
+ throw new Error('the change would leave the record with nobody able to seal it')
515
+ }
516
+ // EL DOBLE FILTRO (dueño, 2026-08-31): un acta nueva tiene que pasar el filtro de la
517
+ // ANTERIOR —quién podía sellar, arriba— y también el de LA QUE SE VA A FIRMAR. Si no
518
+ // pasa los dos, no se firma.
519
+ //
520
+ // Lo que cierra: un sellador que se quita a sí mismo el permiso en la misma acta que
521
+ // firma. Es absurdo —se firma con una autoridad que uno acaba de retirarse— y no tiene
522
+ // vuelta atrás: deshacerlo requeriría sellar, que es justo lo que se acaba de perder.
523
+ //
524
+ // Y por qué aquí y no con una marca de «traspaso»: mirando el acta SOLA, ceder el mando
525
+ // y quitarse el permiso se ven igual. Con las dos actas delante no hay ambigüedad, y las
526
+ // dos veces que esto importa —construirla, y adoptar una ajena— se tienen las dos.
527
+ if (!canSeal(next, by)) {
528
+ throw new Error('the change would leave whoever seals it unable to seal: a record must pass the filter of the one it replaces AND its own')
498
529
  }
499
530
  return next
500
531
  }
@@ -761,21 +792,11 @@ export async function canAdopt ({ candidate, current }) {
761
792
  // exactamente lo que dice que las dos salieron de ella.
762
793
  const mismoFirmante = candidate.sealedBy === current.sealedBy
763
794
  if (!mismoFirmante && !canSeal(current, candidate.sealedBy)) return { adopt: false, reason: 'otro-sellador' }
764
- // UN TRASPASO ES EL QUE CAMBIA A QUIÉN NOMBRA, no el que firma otro. Con una sola
765
- // bóveda daba igual —solo un traspaso podía firmar sin ser el sellador—, pero con dos
766
- // la segunda firma sin serlo cada vez que sella, y entonces TODA acta suya se leía
767
- // como un traspaso y ganaba el desempate por la puerta de atrás.
768
- //
769
- // Las dos ramas salen del mismo padre, así que comparar a quién nombra cada una
770
- // resuelve los tres casos: si nombran al mismo (dos coselladores trabajando), no hay
771
- // traspaso y decide el hash; si una nombra a otro, esa es la que traspasa y gana; y
772
- // si las dos traspasan a destinos distintos, vuelven a empatar y decide el hash,
773
- // que es lo que §2.4.1 ya decía.
774
- const mismoSellador = candidate.sealer === current.sealer
775
- const canT = isHandover(candidate) && !mismoSellador
776
- const curT = isHandover(current) && !mismoSellador
777
- if (canT && !curT) return { adopt: true, reason: 'traspaso-gana' }
778
- if (!canT && curT) return { adopt: false, reason: 'traspaso-gana' }
795
+ // SOLO QUEDA EL HASH. Antes había una regla más —«gana la que traspasa»— que existía
796
+ // para arbitrar el campo `sealer`: dos ramas que cedían el mando a destinos distintos.
797
+ // Ese campo ya no existe (dueño, 2026-08-31: «SEAL es el nuevo master»), y con él se
798
+ // va la regla y la ambigüedad que arrastraba: `sealer !== sealedBy` significaba
799
+ // «traspaso» con un solo master y dejó de significarlo con varios selladores.
779
800
  return hCan < hCur ? { adopt: true, reason: 'desempate-hash' } : { adopt: false, reason: 'desempate-hash' }
780
801
  }
781
802
 
@@ -783,7 +804,7 @@ export async function canAdopt ({ candidate, current }) {
783
804
  }
784
805
 
785
806
  export default {
786
- ACTA_V, CAPS, CAP_SCOPE, genesisActa, actaBody, actaHash, memberId, checkShape, isHandover,
807
+ ACTA_V, CAPS, CAP_SCOPE, genesisActa, actaBody, actaHash, memberId, checkShape,
787
808
  sealActa, verifyActa, applyChanges, makeRenounce, verifyRenounce,
788
809
  makeContinuity, verifyContinuity,
789
810
  cardBody, makeProfileCard, verifyProfileCard, canAdoptCard, sealersOf, canSeal,
package/vault/core.js CHANGED
@@ -540,7 +540,12 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
540
540
  const saveRenounces = (l) => kv.setItem(RENOUNCE_STORAGE, JSON.stringify(l))
541
541
 
542
542
  /** ¿Es ESTE dispositivo el master (el único que puede sellar)? */
543
- const amMaster = () => loadActa()?.sealer === publickeyJwkStr
543
+ /**
544
+ * ¿PUEDO SELLAR? Antes era «¿soy el master?» y miraba un campo. Con sellar convertido en
545
+ * permiso, la pregunta correcta es esta — y su respuesta puede ser «sí» en más de un
546
+ * aparato a la vez, que es justo el punto del multivault.
547
+ */
548
+ const amMaster = () => Acta.canSeal(loadActa(), publickeyJwkStr, loadRenounces())
544
549
 
545
550
  /**
546
551
  * Quien sella SOBRES (la bóveda) pone aquí una función que estrena una llave de sellado
@@ -571,7 +576,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
571
576
  const sealed = await seal(next)
572
577
  pushHistory(acta) // la que deja de ser vigente entra en la ventana de retención
573
578
  saveActa(sealed)
574
- emitVault({ phase: 'acta', seq: sealed.seq, sealer: sealed.sealer })
579
+ emitVault({ phase: 'acta', seq: sealed.seq, sealedBy: sealed.sealedBy })
575
580
  return sealed
576
581
  }
577
582
 
@@ -697,7 +702,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
697
702
  // retener; guardarla aquí solo mezclaría dos cuentas en la misma ventana (§1.3).
698
703
  kv.setItem(ACTA_HISTORY_STORAGE, '[]')
699
704
  clearPendingJoin()
700
- emitVault({ phase: 'acta', seq: candidate.seq, sealer: candidate.sealer, joined: true })
705
+ emitVault({ phase: 'acta', seq: candidate.seq, sealedBy: candidate.sealedBy, joined: true })
701
706
  return { joined: true, profileId: candidate.profileId, seq: candidate.seq }
702
707
  }
703
708
 
@@ -763,7 +768,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
763
768
  const r = await Acta.canAdopt({ candidate, current })
764
769
  if (!r.adopt) return { adopted: false, reason: r.reason, seq: current?.seq ?? null }
765
770
  saveActa(candidate)
766
- emitVault({ phase: 'acta', seq: candidate.seq, sealer: candidate.sealer, adopted: r.reason })
771
+ emitVault({ phase: 'acta', seq: candidate.seq, sealedBy: candidate.sealedBy, adopted: r.reason })
767
772
  const sigoDentro = (candidate.members || []).some((m) => m?.pub === publickeyJwkStr)
768
773
  if (!sigoDentro) {
769
774
  console.warn('[identity] the new record no longer lists this device: removing the account')
@@ -1025,7 +1030,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1025
1030
  async function askIfStillAMember () {
1026
1031
  try {
1027
1032
  const acta = loadActa()
1028
- if (!acta || acta.sealer === publickeyJwkStr) return // no hay cuenta ajena que confirmar: mando yo
1033
+ if (!acta || Acta.canSeal(acta, publickeyJwkStr)) return // sello yo: no hay cuenta ajena que confirmar
1029
1034
  // El acta que ya tengo no me nombra: no hace falta preguntar nada, el acta manda y va
1030
1035
  // firmada. (Normalmente esto lo resuelve `adoptActa` al recibirla.)
1031
1036
  if (!(acta.members || []).some((m) => m?.pub === publickeyJwkStr)) {
@@ -1035,7 +1040,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1035
1040
  const v = loadVaultCert()
1036
1041
  if (v?.cert && loadVaultDevice()) return // con papel, ya lo comprueba el camino normal
1037
1042
  const r = await remoteCheck({
1038
- master: acta.sealer,
1043
+ master: Acta.sealersOf(acta)[0] || null,
1039
1044
  proxy: v?.proxy || 'wss://proxy.dotrino.com',
1040
1045
  device: { publickey: publickeyJwkStr, privateKey: keypair.privateKey },
1041
1046
  onRevoked: wipeVaultLink
@@ -1493,7 +1498,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1493
1498
 
1494
1499
  async profileMembers () {
1495
1500
  const acta = loadActa()
1496
- if (!acta) return { members: [], profileId: null, seq: 0, sealer: null }
1501
+ if (!acta) return { members: [], profileId: null, seq: 0, sealers: [] }
1497
1502
  const pend = loadRenounces()
1498
1503
  const members = await Promise.all(acta.members.map(async (m) => ({
1499
1504
  pub: m.pub,
@@ -1504,7 +1509,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1504
1509
  caps: Acta.effectiveCaps(acta, m.pub, pend),
1505
1510
  addedAt: m.addedAt || null,
1506
1511
  isMe: m.pub === publickeyJwkStr,
1507
- isMaster: m.pub === acta.sealer,
1512
+ isMaster: Acta.canSeal(acta, m.pub),
1508
1513
  // La llave de CIFRADO del miembro y, derivado, si se le puede envolver algo.
1509
1514
  // Sale en la proyección porque sin ella ninguna interfaz (CLI, TUI, consola)
1510
1515
  // puede decir «a este aparato no se le puede escribir», y ese silencio es
@@ -1512,7 +1517,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1512
1517
  encPub: m.encPub || null,
1513
1518
  canSeal: !!m.encPub
1514
1519
  })))
1515
- return { members, profileId: acta.profileId, seq: acta.seq, sealer: acta.sealer, updatedAt: acta.updatedAt }
1520
+ return { members, profileId: acta.profileId, seq: acta.seq, sealers: Acta.sealersOf(acta), updatedAt: acta.updatedAt }
1516
1521
  },
1517
1522
 
1518
1523
  async myMembership () {
@@ -1523,7 +1528,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1523
1528
  inProfile: !!m,
1524
1529
  profileId: acta.profileId,
1525
1530
  seq: acta.seq,
1526
- isMaster: acta.sealer === publickeyJwkStr,
1531
+ isMaster: Acta.canSeal(acta, publickeyJwkStr),
1527
1532
  caps: Acta.effectiveCaps(acta, publickeyJwkStr, loadRenounces()),
1528
1533
  id: m ? await Acta.memberId(m.pub) : null
1529
1534
  }
@@ -1590,12 +1595,24 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1590
1595
  * sellador tiene que ser miembro para poder serlo, y así no hay ventana intermedia.
1591
1596
  * Cubre igual dispositivo → bóveda y bóveda → bóveda (mudarse de PC).
1592
1597
  */
1598
+ /**
1599
+ * CEDER EL MANDO = CONCEDER `sella`. Ya no hay traspaso: sellar es un permiso, así que
1600
+ * «pasarle el mando a otro» es dárselo, y punto. Lo que ANTES hacía esto —dejar de
1601
+ * poder sellar uno mismo, en el mismo acto— ya no se puede: nadie se quita el sello a
1602
+ * sí mismo (dejaría un acta que él firma sin poder firmarla). Si quieres salir, se lo
1603
+ * pides al otro cuando ya pueda sellar.
1604
+ */
1593
1605
  async handoverMaster ({ to, member = null } = {}) {
1594
1606
  const changes = []
1595
- if (member) changes.push({ op: 'admit', member: { ...member, pub: to } })
1596
- changes.push({ op: 'handover', to })
1607
+ const acta0 = loadActa()
1608
+ if (member) changes.push({ op: 'admit', member: { ...member, pub: to, caps: [...(member.caps || []), 'sealer'] } })
1609
+ else {
1610
+ const m = (acta0?.members || []).find((x) => x.pub === to)
1611
+ if (!m) throw new Error('handover: the new sealer must be a member (admit them in the same change)')
1612
+ changes.push({ op: 'caps', pub: to, caps: [...new Set([...(m.caps || []), 'sealer'])] })
1613
+ }
1597
1614
  const acta = await sealChanges(changes)
1598
- return { ok: true, seq: acta.seq, sealer: acta.sealer }
1615
+ return { ok: true, seq: acta.seq, sealers: Acta.sealersOf(acta) }
1599
1616
  },
1600
1617
 
1601
1618
  /**
@@ -1606,6 +1623,13 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1606
1623
  async renounceCaps ({ caps } = {}) {
1607
1624
  const acta = loadActa()
1608
1625
  if (!acta) throw new Error('this profile has no record yet')
1626
+ // SELLAR NO SE RENUNCIA. Es la misma auto-amputación que el acta ya impide, por otra
1627
+ // puerta: la renuncia es unilateral y no pasa por nadie, así que sin esto un
1628
+ // sellador podría dejarse —o dejar a la cuenta— sin nadie que pueda volver a sellar,
1629
+ // y eso no tiene marcha atrás. Que te lo quite otro sellador, que sí puede.
1630
+ if ((Array.isArray(caps) ? caps : [caps]).includes('sealer')) {
1631
+ throw new Error('sealing cannot be renounced: ask another sealer to take it from you')
1632
+ }
1609
1633
  const record = await Acta.makeRenounce({ member: publickeyJwkStr, caps, privateKey: keypair.privateKey })
1610
1634
  const pend = loadRenounces().filter((r) => r.member !== publickeyJwkStr)
1611
1635
  pend.push(record)
@@ -1735,7 +1759,11 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1735
1759
  // hasta ella: en los dos casos, mandarla es tirar cientos de KB por el transporte.
1736
1760
  if (!mine) return { chain: [], window: ACTA_WINDOW }
1737
1761
  // Puede dar el salto él solo: el sellador no cambió.
1738
- if (cur.sealedBy === mine.sealer) return { chain: [], window: ACTA_WINDOW }
1762
+ // Si quien selló la última YA podía sellar en el acta que tiene el otro, el salto lo
1763
+ // da él solo: no hace falta mandarle los eslabones. Antes esto comparaba contra el
1764
+ // campo `sealer`; ahora se le pregunta al permiso, que es lo mismo con un solo
1765
+ // sellador y lo correcto con varios.
1766
+ if (Acta.canSeal(mine, cur.sealedBy)) return { chain: [], window: ACTA_WINDOW }
1739
1767
  const all = [...hist.filter((a) => a.seq > sinceSeq), cur]
1740
1768
  return { chain: all.sort((a, b) => a.seq - b.seq), window: ACTA_WINDOW }
1741
1769
  },