@dotrino/identity 0.67.0 → 0.69.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/src/index.js +2 -0
- package/src/node.js +2 -0
- package/vault/acta.js +108 -3
- package/vault/core.js +60 -3
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -278,6 +278,8 @@ export class Identity {
|
|
|
278
278
|
|
|
279
279
|
/** El acta vigente + si este dispositivo es el master + sus capacidades efectivas. */
|
|
280
280
|
async profileActa () { return this._call('profileActa') }
|
|
281
|
+
/** La cadena de selladores, para mandarla con una firma (ver `vault/acta.js`). */
|
|
282
|
+
async sealerChain () { return this._call('sealerChain') }
|
|
281
283
|
|
|
282
284
|
/** Miembros del perfil, ya con id legible y capacidades efectivas. */
|
|
283
285
|
async profileMembers () { return this._call('profileMembers') }
|
package/src/node.js
CHANGED
|
@@ -169,6 +169,8 @@ export class Identity {
|
|
|
169
169
|
listDelegations () { return this._h('listDelegations') }
|
|
170
170
|
// Acta de perfil (qué llaves son del perfil y qué puede cada una; ver acta-de-perfil.md)
|
|
171
171
|
profileActa () { return this._h('profileActa') }
|
|
172
|
+
/** La cadena de selladores, para mandarla con una firma (ver `vault/acta.js`). */
|
|
173
|
+
sealerChain () { return this._h('sealerChain') }
|
|
172
174
|
profileMembers () { return this._h('profileMembers') }
|
|
173
175
|
/**
|
|
174
176
|
* Quien SELLA SOBRES (la bóveda) registra aquí cómo estrenar su llave de sellado: se
|
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 = 4
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Versiones de acta que se ACEPTAN al leer. La 1 sigue entrando porque una v1 en el disco
|
|
@@ -57,7 +57,11 @@ export const ACTA_V = 3
|
|
|
57
57
|
* entiende; la PRIMERA vez que esa cuenta cambie algo, la nueva sale ya en v3 y el campo
|
|
58
58
|
* desaparece solo.
|
|
59
59
|
*/
|
|
60
|
-
const ACTA_LEIBLES = Object.freeze([1, 2, 3])
|
|
60
|
+
const ACTA_LEIBLES = Object.freeze([1, 2, 3, 4])
|
|
61
|
+
/** Desde esta versión, el acta lleva su eslabón de la cadena de selladores. */
|
|
62
|
+
const V_CON_CADENA = 4
|
|
63
|
+
/** ¿Esta acta lleva el eslabón? Las anteriores se leen igual; no pueden encadenar. */
|
|
64
|
+
const conCadena = (acta) => Number(acta?.v) >= V_CON_CADENA
|
|
61
65
|
/** Desde esta versión, sellar es solo un permiso y no hay campo `sealer`. */
|
|
62
66
|
const V_SIN_CAMPO_SELLADOR = 3
|
|
63
67
|
/** ¿Este acta es de las viejas, con el campo? */
|
|
@@ -225,6 +229,12 @@ export function genesisActa ({ pub, encPub = null, sealPub = null, label = '', n
|
|
|
225
229
|
sealedBy: pub,
|
|
226
230
|
seq: 1,
|
|
227
231
|
prev: null,
|
|
232
|
+
// LA CADENA DE SELLADORES. El génesis es el ancla: está autofirmado por la llave que
|
|
233
|
+
// da nombre al perfil, así que no apunta a nada y no hay nada que falsificar sin ella.
|
|
234
|
+
sealerAnchor: null,
|
|
235
|
+
// `true` porque el génesis ESTABLECE el primer conjunto de selladores: es el eslabón
|
|
236
|
+
// 1 de la cadena, y por eso la siguiente acta tiene que apuntarle a él.
|
|
237
|
+
sealerChanged: true,
|
|
228
238
|
members: [{ pub, encPub, label: String(label || '').slice(0, 60), cn: null, caps: [...PAIRED_CAPS, 'sealer'], addedAt: now, cert: null }],
|
|
229
239
|
revoked: [],
|
|
230
240
|
renounced: [],
|
|
@@ -294,6 +304,18 @@ export function checkShape (acta) {
|
|
|
294
304
|
// Es el hermano del cierre de `sign`, y sustituye a «el sellador tiene que ser miembro»
|
|
295
305
|
// ahora que sellar es solo un permiso y los permisos ya son de miembros por definición.
|
|
296
306
|
if (!sealersOf(acta).length) return 'sin-sellador'
|
|
307
|
+
// La cadena de selladores: el génesis es el ancla (`null`); cualquier otra apunta a un
|
|
308
|
+
// eslabón anterior. Sin `hash` no se puede encadenar nada, así que se exige la forma.
|
|
309
|
+
if (acta.sealerAnchor != null) {
|
|
310
|
+
const a = acta.sealerAnchor
|
|
311
|
+
if (typeof a !== 'object' || !Number.isInteger(a.seq) || a.seq < 1 || a.seq >= acta.seq) return 'sealeranchor'
|
|
312
|
+
if (typeof a.hash !== 'string' || !/^[0-9a-f]{64}$/.test(a.hash)) return 'sealeranchor'
|
|
313
|
+
}
|
|
314
|
+
// El ancla NO se exige: una cuenta que viene de antes de v4 escribe su primera acta
|
|
315
|
+
// nueva sin poder apuntar a nada (su padre no era eslabón), y exigirla la dejaría
|
|
316
|
+
// inválida. Lo que sostiene la cadena no es el ancla sino que **cada eslabón lo selle
|
|
317
|
+
// alguien a quien el anterior autorizaba**; el ancla añade que no se pueda empalmar un
|
|
318
|
+
// eslabón de otra rama, y se comprueba cuando viene (ver `verifySealerChain`).
|
|
297
319
|
if (!acta.members.some((m) => m.caps.includes('sign'))) return 'sin-firmante'
|
|
298
320
|
return null
|
|
299
321
|
}
|
|
@@ -315,6 +337,60 @@ export async function sealActa ({ acta, privateKey, privateJwk }) {
|
|
|
315
337
|
* decide aquí sino al adoptarla (`canAdopt`), comparándola con la que ya tienes.
|
|
316
338
|
* @returns {{ok:boolean, reason?:string}}
|
|
317
339
|
*/
|
|
340
|
+
/**
|
|
341
|
+
* ¿HABLA ESTA ACTA POR ESTA IDENTIDAD? Lo que comprueba un EXTRAÑO, sin saber nada de ti.
|
|
342
|
+
*
|
|
343
|
+
* Recibe la cadena de selladores: `[génesis, …cambios de sellador…, acta actual]`. No son
|
|
344
|
+
* todas tus actas —eso crecería con cada emparejamiento— sino solo los eslabones donde
|
|
345
|
+
* cambió quién sella, que casi nunca pasa. Lo normal es longitud 1 o 2.
|
|
346
|
+
*
|
|
347
|
+
* Qué se comprueba, y por qué basta:
|
|
348
|
+
* 1. el primer eslabón es el GÉNESIS y está autofirmado por `profileId`. Ese es el
|
|
349
|
+
* ancla: sin esa llave no se puede fabricar, y el `profileId` ya lo conoce quien
|
|
350
|
+
* verifica (viaja en cada firma tuya);
|
|
351
|
+
* 2. cada eslabón siguiente lo selló alguien a quien el ANTERIOR autorizaba;
|
|
352
|
+
* 3. y apunta al anterior por `seq` + hash, así que no se puede colar uno de otra rama.
|
|
353
|
+
*
|
|
354
|
+
* Sin esto, un acta suelta es falsificable: con tu `profileId` cualquiera fabrica una
|
|
355
|
+
* donde él sella, y `verifyActa` la da por buena — está firmada, solo que por él.
|
|
356
|
+
*
|
|
357
|
+
* Lo que NO resuelve, y no lo esconde: la frescura. Quien guardó una cadena vieja sigue
|
|
358
|
+
* aceptando a un sellador que ya retiraste. Es el problema de siempre y hace falta un
|
|
359
|
+
* oráculo, no más firmas.
|
|
360
|
+
*/
|
|
361
|
+
export async function verifySealerChain (chain, { expectedProfileId = null } = {}) {
|
|
362
|
+
if (!Array.isArray(chain) || !chain.length) return { ok: false, reason: 'cadena-vacia' }
|
|
363
|
+
const [raiz] = chain
|
|
364
|
+
|
|
365
|
+
if (raiz.seq !== 1 || raiz.sealerAnchor != null) return { ok: false, reason: 'no-empieza-en-genesis' }
|
|
366
|
+
// El ancla: el génesis se firma a sí mismo con la llave que da nombre al perfil.
|
|
367
|
+
if (raiz.sealedBy !== raiz.profileId) return { ok: false, reason: 'genesis-no-autofirmado' }
|
|
368
|
+
if (expectedProfileId != null && raiz.profileId !== expectedProfileId) return { ok: false, reason: 'otro-perfil' }
|
|
369
|
+
const vr = await verifyActa({ acta: raiz })
|
|
370
|
+
if (!vr.ok) return { ok: false, reason: 'genesis:' + vr.reason }
|
|
371
|
+
|
|
372
|
+
for (let i = 1; i < chain.length; i++) {
|
|
373
|
+
const previo = chain[i - 1]
|
|
374
|
+
const actual = chain[i]
|
|
375
|
+
if (actual.profileId !== raiz.profileId) return { ok: false, reason: 'otro-perfil' }
|
|
376
|
+
const v = await verifyActa({ acta: actual })
|
|
377
|
+
if (!v.ok) return { ok: false, reason: `eslabon-${i}:` + v.reason }
|
|
378
|
+
// Encadena con el anterior: mismo `seq` y mismo hash. Sin el hash bastaría con
|
|
379
|
+
// acertar un número para colar un eslabón de otra rama.
|
|
380
|
+
// El ancla, SI viene: fija que este eslabón sale del anterior y no de otra rama. Una
|
|
381
|
+
// cuenta anterior a v4 puede no traerla, y entonces lo que sostiene la cadena es lo de
|
|
382
|
+
// abajo — que es lo que de verdad impide falsificarla.
|
|
383
|
+
const a = actual.sealerAnchor
|
|
384
|
+
if (a && (a.seq !== previo.seq || a.hash !== await actaHash(previo))) {
|
|
385
|
+
return { ok: false, reason: `eslabon-${i}:no-encadena` }
|
|
386
|
+
}
|
|
387
|
+
// Y lo que da la autoridad: quien la selló tenía permiso SEGÚN EL ESLABÓN ANTERIOR.
|
|
388
|
+
if (!canSeal(previo, actual.sealedBy)) return { ok: false, reason: `eslabon-${i}:sellador-no-autorizado` }
|
|
389
|
+
}
|
|
390
|
+
const ultima = chain[chain.length - 1]
|
|
391
|
+
return { ok: true, profileId: raiz.profileId, seq: ultima.seq, sealers: sealersOf(ultima) }
|
|
392
|
+
}
|
|
393
|
+
|
|
318
394
|
export async function verifyActa ({ acta, expectedProfileId } = {}) {
|
|
319
395
|
const shape = checkShape(acta)
|
|
320
396
|
if (shape) return { ok: false, reason: shape }
|
|
@@ -513,6 +589,35 @@ export async function applyChanges (acta, changes, { by, now = Date.now(), sealP
|
|
|
513
589
|
if (!sealersOf(next).length) {
|
|
514
590
|
throw new Error('the change would leave the record with nobody able to seal it')
|
|
515
591
|
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* LA CADENA DE SELLADORES. Es lo que deja a un EXTRAÑO comprobar que quien firmó esto
|
|
595
|
+
* habla por esta identidad, sin tener que mandarle todas las actas.
|
|
596
|
+
*
|
|
597
|
+
* El problema: el acta suelta es falsificable —con tu `profileId`, que es público,
|
|
598
|
+
* cualquiera fabrica una donde él sella—. Y mandar la cadena entera no sale a cuenta
|
|
599
|
+
* porque crece con cada emparejamiento, que es lo que más cambia.
|
|
600
|
+
*
|
|
601
|
+
* La salida: solo encadenar los eslabones donde CAMBIA quién sella, que casi nunca pasa.
|
|
602
|
+
* Un usuario normal tiene cadena de longitud 1 —el génesis— para siempre; con una
|
|
603
|
+
* segunda bóveda, 2. Ese es el tamaño real, no el número de actas.
|
|
604
|
+
*
|
|
605
|
+
* Cada acta apunta al último cambio ANTERIOR a ella, nunca a sí misma (sería circular:
|
|
606
|
+
* el puntero va dentro de lo que se firma). Y `sealerChanged` dice si ELLA es uno, para
|
|
607
|
+
* que la siguiente sepa a dónde apuntar teniendo solo a su padre delante.
|
|
608
|
+
*
|
|
609
|
+
* Y lo que esto resuelve y el génesis firmando no resolvía: **no hace falta la llave del
|
|
610
|
+
* génesis para sumar un sellador.** La bóveda que ya está autorizada sella el acta que
|
|
611
|
+
* autoriza a la siguiente, y ese eslabón vale porque el anterior la autorizaba a ella.
|
|
612
|
+
* El génesis firmó el eslabón 1 y puede estar perdido desde entonces — que es justo el
|
|
613
|
+
* desastre para el que existe el multivault.
|
|
614
|
+
*/
|
|
615
|
+
const antes = sealersOf(acta).slice().sort().join('|')
|
|
616
|
+
const despues = sealersOf(next).slice().sort().join('|')
|
|
617
|
+
next.sealerChanged = antes !== despues
|
|
618
|
+
next.sealerAnchor = acta.sealerChanged
|
|
619
|
+
? { seq: acta.seq, hash: await actaHash(acta) }
|
|
620
|
+
: (acta.sealerAnchor || null)
|
|
516
621
|
// EL DOBLE FILTRO (dueño, 2026-08-31): un acta nueva tiene que pasar el filtro de la
|
|
517
622
|
// ANTERIOR —quién podía sellar, arriba— y también el de LA QUE SE VA A FIRMAR. Si no
|
|
518
623
|
// pasa los dos, no se firma.
|
|
@@ -804,7 +909,7 @@ export async function canAdopt ({ candidate, current }) {
|
|
|
804
909
|
}
|
|
805
910
|
|
|
806
911
|
export default {
|
|
807
|
-
ACTA_V, CAPS, CAP_SCOPE, genesisActa, actaBody, actaHash, memberId, checkShape,
|
|
912
|
+
ACTA_V, CAPS, CAP_SCOPE, genesisActa, actaBody, actaHash, memberId, checkShape, verifySealerChain,
|
|
808
913
|
sealActa, verifyActa, applyChanges, makeRenounce, verifyRenounce,
|
|
809
914
|
makeContinuity, verifyContinuity,
|
|
810
915
|
cardBody, makeProfileCard, verifyProfileCard, canAdoptCard, sealersOf, canSeal,
|
package/vault/core.js
CHANGED
|
@@ -528,12 +528,48 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
528
528
|
// que la nueva desciende de la que uno tenía. Más viejo que la ventana ⇒ re-admitirse.
|
|
529
529
|
const ACTA_WINDOW = 50
|
|
530
530
|
const loadHistory = () => { try { return JSON.parse(kv.getItem(ACTA_HISTORY_STORAGE) || '[]') || [] } catch (_) { return [] } }
|
|
531
|
+
/**
|
|
532
|
+
* LOS ESLABONES DE LA CADENA DE SELLADORES NO CADUCAN (dueño, 2026-08-31).
|
|
533
|
+
*
|
|
534
|
+
* El resto sí: la ventana existe para que un miembro que estuvo apagado compruebe el
|
|
535
|
+
* encadenamiento al volver, y para eso las últimas bastan. Pero los eslabones donde
|
|
536
|
+
* CAMBIÓ quién sella son otra cosa — son lo que deja a un EXTRAÑO anclar en el génesis
|
|
537
|
+
* y saber que hablas tú. Si la poda se los lleva, nadie de fuera puede verificar nada
|
|
538
|
+
* tuyo nunca más, y no hay forma de reconstruirlos.
|
|
539
|
+
*
|
|
540
|
+
* El comentario de la ventana decía «un tercero no las necesita, le basta el snapshot
|
|
541
|
+
* actual». Era cierto mientras el ancla fuera una llave fija; dejó de serlo en cuanto
|
|
542
|
+
* varias llaves pueden sellar.
|
|
543
|
+
*
|
|
544
|
+
* No crecen: solo suman al añadir o quitar una bóveda, que casi nunca pasa. Un usuario
|
|
545
|
+
* normal guarda uno —el génesis— para siempre.
|
|
546
|
+
*/
|
|
531
547
|
const pushHistory = (acta) => {
|
|
532
548
|
if (!acta) return
|
|
533
549
|
const h = loadHistory().filter((a) => a.seq !== acta.seq)
|
|
534
550
|
h.push(acta)
|
|
535
551
|
h.sort((a, b) => a.seq - b.seq)
|
|
536
|
-
|
|
552
|
+
const ventana = h.slice(-ACTA_WINDOW)
|
|
553
|
+
const enVentana = new Set(ventana.map((a) => a.seq))
|
|
554
|
+
const eslabones = h.filter((a) => a.sealerChanged && !enVentana.has(a.seq))
|
|
555
|
+
kv.setItem(ACTA_HISTORY_STORAGE, JSON.stringify([...eslabones, ...ventana].sort((a, b) => a.seq - b.seq)))
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* La CADENA DE SELLADORES para mandarla con una firma: `[génesis, …cambios…, actual]`.
|
|
560
|
+
* Es lo que un extraño necesita para anclar en el `profileId`, y es corta a propósito —
|
|
561
|
+
* no lleva las actas de emparejar aparatos, que son casi todas.
|
|
562
|
+
*/
|
|
563
|
+
const sealerChain = () => {
|
|
564
|
+
const cur = loadActa()
|
|
565
|
+
if (!cur) return []
|
|
566
|
+
// La vigente cuenta como parte del material aunque todavía no esté en la historia
|
|
567
|
+
// (ahí entra al dejar de serlo). Sin esto, una cuenta recién creada devolvía [].
|
|
568
|
+
const porSeq = new Map([...loadHistory(), cur].map((a) => [a.seq, a]))
|
|
569
|
+
const eslabones = [...porSeq.values()].filter((a) => a.sealerChanged).sort((a, b) => a.seq - b.seq)
|
|
570
|
+
// Y la actual va al final si ella misma no es un eslabón: el verificador necesita ver
|
|
571
|
+
// la cabeza para saber en qué `seq` está y quién la selló.
|
|
572
|
+
return eslabones.length && eslabones[eslabones.length - 1].seq === cur.seq ? eslabones : [...eslabones, cur]
|
|
537
573
|
}
|
|
538
574
|
|
|
539
575
|
const loadRenounces = () => { try { return JSON.parse(kv.getItem(RENOUNCE_STORAGE) || '[]') || [] } catch (_) { return [] } }
|
|
@@ -596,7 +632,14 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
596
632
|
const { generation } = await Content.makeGeneration({ members: base.members, gen: 1 })
|
|
597
633
|
base.keyring = [generation]
|
|
598
634
|
} catch (e) { console.warn('[identity] could not create the content key:', e.message) }
|
|
599
|
-
|
|
635
|
+
const genesis = await seal(base)
|
|
636
|
+
saveActa(genesis)
|
|
637
|
+
// EL GÉNESIS VA A LA HISTORIA DESDE EL PRIMER DÍA. Es el ancla de la cadena de
|
|
638
|
+
// selladores —el único eslabón autofirmado por la llave que da nombre al perfil— y
|
|
639
|
+
// sin él nadie de fuera puede verificar nada de esta cuenta, nunca. Antes solo se
|
|
640
|
+
// guardaba lo que DEJABA de ser vigente, así que una cuenta que nunca cambió su acta
|
|
641
|
+
// no tenía génesis guardado y su cadena salía vacía.
|
|
642
|
+
pushHistory(genesis)
|
|
600
643
|
return loadActa()
|
|
601
644
|
}
|
|
602
645
|
|
|
@@ -1113,7 +1156,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1113
1156
|
const LOCK_EXEMPT = new Set([
|
|
1114
1157
|
'profileLockStatus', 'unlockProfile', 'listProfiles', 'currentProfile',
|
|
1115
1158
|
'switchProfile', 'createProfile',
|
|
1116
|
-
'profileActa', 'profileMembers', 'myMembership', 'isMaster'
|
|
1159
|
+
'profileActa', 'profileMembers', 'myMembership', 'isMaster', 'sealerChain'
|
|
1117
1160
|
])
|
|
1118
1161
|
|
|
1119
1162
|
const handlers = {
|
|
@@ -1467,6 +1510,20 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1467
1510
|
return { acta, isMaster: amMaster(), myCaps: Acta.effectiveCaps(acta, publickeyJwkStr, loadRenounces()) }
|
|
1468
1511
|
},
|
|
1469
1512
|
|
|
1513
|
+
/**
|
|
1514
|
+
* LA CADENA DE SELLADORES, para mandarla junto con una firma.
|
|
1515
|
+
*
|
|
1516
|
+
* Es lo que deja a un EXTRAÑO comprobar que quien firmó habla por esta identidad, sin
|
|
1517
|
+
* preguntarle a nadie: `[génesis, …cambios de sellador…, acta actual]`. No son todas
|
|
1518
|
+
* las actas —eso crecería con cada emparejamiento— sino solo los eslabones donde
|
|
1519
|
+
* cambió quién sella, que casi nunca pasa. Lo normal es longitud 1: el génesis.
|
|
1520
|
+
*
|
|
1521
|
+
* Una cuenta de una sola bóveda devuelve SIEMPRE eso y nada más, y no puede quedarse
|
|
1522
|
+
* obsoleta: su conjunto de selladores no puede cambiar, porque el único no puede
|
|
1523
|
+
* quitarse a sí mismo y no hay otro que se lo quite.
|
|
1524
|
+
*/
|
|
1525
|
+
async sealerChain () { return sealerChain() },
|
|
1526
|
+
|
|
1470
1527
|
/**
|
|
1471
1528
|
* Quien sella sobres de secretos (la bóveda) registra aquí cómo estrenar su LLAVE DE
|
|
1472
1529
|
* SELLADO: una función que crea el par, se guarda la privada y devuelve la pública. Se
|