@dotrino/identity 0.65.1 → 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 +1 -1
- package/vault/acta.js +92 -46
- package/vault/core.js +42 -14
package/package.json
CHANGED
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 =
|
|
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
|
-
|
|
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
|
|
186
|
-
.filter((m) =>
|
|
200
|
+
const porPermiso = (acta.members || [])
|
|
201
|
+
.filter((m) => memberCan(acta, m.pub, 'sealer', renounces))
|
|
187
202
|
.map((m) => m.pub)
|
|
188
|
-
|
|
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 && (
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
497
|
-
|
|
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
|
}
|
|
@@ -578,6 +609,31 @@ export function memberCanSign (acta, pub, ns = null) {
|
|
|
578
609
|
return ns != null && m.cn === ns
|
|
579
610
|
}
|
|
580
611
|
|
|
612
|
+
/**
|
|
613
|
+
* ¿PUEDE ESTE MIEMBRO LO QUE PIDE EL SCOPE? La misma pregunta para todos los mostradores.
|
|
614
|
+
*
|
|
615
|
+
* El certificado dice a qué se comprometió la bóveda cuando conectó el aparato; el ACTA
|
|
616
|
+
* dice lo que puede hoy. Y no coinciden: cambiar los permisos sella el acta pero NO
|
|
617
|
+
* reemite ni revoca el papel, que vive hasta 30 días. Quitarle `lee` a un aparato y que
|
|
618
|
+
* siguiera leyendo un mes es el mismo fallo que estaba en `sign`, en cada mostrador que
|
|
619
|
+
* se olvidara de preguntar.
|
|
620
|
+
*
|
|
621
|
+
* Por eso esto existe y por eso traduce el SCOPE (el idioma del certificado) a la
|
|
622
|
+
* capacidad (el idioma del acta): así el guardia se pone UNA vez, junto a la verificación
|
|
623
|
+
* de la cadena, y un mostrador nuevo lo hereda en lugar de tener que acordarse.
|
|
624
|
+
*
|
|
625
|
+
* Devuelve `false` ante un scope que no reconoce: si aparece uno nuevo, lo que toca es
|
|
626
|
+
* añadirlo aquí, no que se cuele por no estar en la lista.
|
|
627
|
+
*/
|
|
628
|
+
export function memberCanScope (acta, pub, scope) {
|
|
629
|
+
if (!acta || typeof scope !== 'string') return false
|
|
630
|
+
const secretos = /^vault:secrets:([a-z0-9-]{1,32})$/.exec(scope)
|
|
631
|
+
if (secretos) return memberCanReadSecrets(acta, pub, secretos[1])
|
|
632
|
+
if (scope === 'vault:sign') return memberCanSign(acta, pub)
|
|
633
|
+
const cap = Object.keys(CAP_SCOPE).find((c) => CAP_SCOPE[c] === scope)
|
|
634
|
+
return cap ? memberCan(acta, pub, cap) : false
|
|
635
|
+
}
|
|
636
|
+
|
|
581
637
|
export function memberCanReadSecrets (acta, pub, ns) {
|
|
582
638
|
const m = (acta?.members || []).find((x) => x.pub === pub)
|
|
583
639
|
if (!m || !m.cn) return false
|
|
@@ -736,21 +792,11 @@ export async function canAdopt ({ candidate, current }) {
|
|
|
736
792
|
// exactamente lo que dice que las dos salieron de ella.
|
|
737
793
|
const mismoFirmante = candidate.sealedBy === current.sealedBy
|
|
738
794
|
if (!mismoFirmante && !canSeal(current, candidate.sealedBy)) return { adopt: false, reason: 'otro-sellador' }
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
// Las dos ramas salen del mismo padre, así que comparar a quién nombra cada una
|
|
745
|
-
// resuelve los tres casos: si nombran al mismo (dos coselladores trabajando), no hay
|
|
746
|
-
// traspaso y decide el hash; si una nombra a otro, esa es la que traspasa y gana; y
|
|
747
|
-
// si las dos traspasan a destinos distintos, vuelven a empatar y decide el hash,
|
|
748
|
-
// que es lo que §2.4.1 ya decía.
|
|
749
|
-
const mismoSellador = candidate.sealer === current.sealer
|
|
750
|
-
const canT = isHandover(candidate) && !mismoSellador
|
|
751
|
-
const curT = isHandover(current) && !mismoSellador
|
|
752
|
-
if (canT && !curT) return { adopt: true, reason: 'traspaso-gana' }
|
|
753
|
-
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.
|
|
754
800
|
return hCan < hCur ? { adopt: true, reason: 'desempate-hash' } : { adopt: false, reason: 'desempate-hash' }
|
|
755
801
|
}
|
|
756
802
|
|
|
@@ -758,9 +804,9 @@ export async function canAdopt ({ candidate, current }) {
|
|
|
758
804
|
}
|
|
759
805
|
|
|
760
806
|
export default {
|
|
761
|
-
ACTA_V, CAPS, CAP_SCOPE, genesisActa, actaBody, actaHash, memberId, checkShape,
|
|
807
|
+
ACTA_V, CAPS, CAP_SCOPE, genesisActa, actaBody, actaHash, memberId, checkShape,
|
|
762
808
|
sealActa, verifyActa, applyChanges, makeRenounce, verifyRenounce,
|
|
763
809
|
makeContinuity, verifyContinuity,
|
|
764
810
|
cardBody, makeProfileCard, verifyProfileCard, canAdoptCard, sealersOf, canSeal,
|
|
765
|
-
effectiveCaps, memberCan, memberCanSign, memberCanReadSecrets, memberScopes, isService, capScope, isValidCn, canAdopt
|
|
811
|
+
effectiveCaps, memberCan, memberCanSign, memberCanScope, memberCanReadSecrets, memberScopes, isService, capScope, isValidCn, canAdopt
|
|
766
812
|
}
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
1596
|
-
changes.push({ op: '
|
|
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,
|
|
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
|
-
|
|
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
|
},
|