@dotrino/identity 0.53.0 → 0.54.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 +7 -4
- package/vault/acta.js +67 -4
- package/vault/core.js +33 -1
- package/src/types.d.ts +0 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/identity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.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",
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"LICENSE"
|
|
40
40
|
],
|
|
41
41
|
"scripts": {
|
|
42
|
-
"test": "node --test test/*.test.js"
|
|
42
|
+
"test": "node --test test/*.test.js",
|
|
43
|
+
"type-check": "tsc --noEmit"
|
|
43
44
|
},
|
|
44
45
|
"keywords": [
|
|
45
46
|
"identity",
|
|
@@ -55,9 +56,11 @@
|
|
|
55
56
|
"url": "git+https://github.com/imdotrino/dotrino-identity.git"
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|
|
58
|
-
"@dotrino/proxy-client": "0.10.
|
|
59
|
+
"@dotrino/proxy-client": "0.10.1"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
|
-
"fake-indexeddb": "^6.2.5"
|
|
62
|
+
"fake-indexeddb": "^6.2.5",
|
|
63
|
+
"typescript": "5.9.3",
|
|
64
|
+
"@types/node": "^22.0.0"
|
|
62
65
|
}
|
|
63
66
|
}
|
package/vault/acta.js
CHANGED
|
@@ -30,7 +30,16 @@
|
|
|
30
30
|
import { canonicalStringify } from './core.js'
|
|
31
31
|
import { signWithDevice, verifyDeviceSig, pubkeyId } from './capabilities.js'
|
|
32
32
|
|
|
33
|
-
export const ACTA_V =
|
|
33
|
+
export const ACTA_V = 2
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Versiones de acta que se ACEPTAN al leer. La 1 sigue entrando porque una v1 en el disco
|
|
37
|
+
* es un perfil con sus aparatos dentro: rechazarla dejaría al vault sin poder verificar a
|
|
38
|
+
* nadie —y a los servicios sin configuración— por un campo que ni siquiera existía. Se
|
|
39
|
+
* asciende sola: el acta siguiente que selle la maestra ya sale v2 (ver `applyChanges`).
|
|
40
|
+
* La única diferencia es la llave de sellado (§8.9), que en una v1 simplemente no hay.
|
|
41
|
+
*/
|
|
42
|
+
const ACTA_LEIBLES = Object.freeze([1, 2])
|
|
34
43
|
|
|
35
44
|
/**
|
|
36
45
|
* Lista CERRADA de capacidades. Sellar y admitir no están: eso es ser el master.
|
|
@@ -126,7 +135,7 @@ export const isHandover = (acta) => !!acta && acta.sealer !== acta.sealedBy
|
|
|
126
135
|
* sellador, con todas las capacidades. `profileId` = su pubkey → el nombre del perfil es
|
|
127
136
|
* estable para siempre y coincide con la identidad que el usuario ya tenía (cero migración).
|
|
128
137
|
*/
|
|
129
|
-
export function genesisActa ({ pub, encPub = null, label = '', now = Date.now() }) {
|
|
138
|
+
export function genesisActa ({ pub, encPub = null, sealPub = null, label = '', now = Date.now() }) {
|
|
130
139
|
if (!isPub(pub)) throw new Error('genesisActa: missing genesis pubkey')
|
|
131
140
|
return {
|
|
132
141
|
v: ACTA_V,
|
|
@@ -141,6 +150,14 @@ export function genesisActa ({ pub, encPub = null, label = '', now = Date.now()
|
|
|
141
150
|
// Llavero del contenido: una entrada por generación, con la clave del perfil ENVUELTA
|
|
142
151
|
// a cada miembro (ver content.js). Envuelto es público: solo lo abre su destinatario.
|
|
143
152
|
keyring: [],
|
|
153
|
+
// LLAVE DE SELLADO (§8.9 de dotrino-vault/docs/secretos-sellados.md): con ella la
|
|
154
|
+
// bóveda FIRMA los sobres de los secretos, para que se sepa que salieron de ella.
|
|
155
|
+
// Vive aquí y no en un certificado aparte porque rota con el acta: cada acta nueva
|
|
156
|
+
// puede nombrar una llave nueva, y quien selle el acta es —siempre— la maestra.
|
|
157
|
+
// Nace en null: un perfil no tiene por qué sellar secretos.
|
|
158
|
+
sealPub: sealPub || null,
|
|
159
|
+
sealSince: sealPub ? 1 : 0,
|
|
160
|
+
sealKeys: [],
|
|
144
161
|
updatedAt: now
|
|
145
162
|
}
|
|
146
163
|
}
|
|
@@ -148,7 +165,7 @@ export function genesisActa ({ pub, encPub = null, label = '', now = Date.now()
|
|
|
148
165
|
/** Comprobaciones de FORMA (sin cripto): que el acta sea un acta. */
|
|
149
166
|
export function checkShape (acta) {
|
|
150
167
|
if (!acta || typeof acta !== 'object') return 'no-acta'
|
|
151
|
-
if (acta.v
|
|
168
|
+
if (!ACTA_LEIBLES.includes(acta.v)) return 'version'
|
|
152
169
|
if (!isPub(acta.profileId) || !isPub(acta.sealer) || !isPub(acta.sealedBy)) return 'shape'
|
|
153
170
|
if (!Number.isInteger(acta.seq) || acta.seq < 1) return 'seq'
|
|
154
171
|
if (acta.seq > 1 && typeof acta.prev !== 'string') return 'prev'
|
|
@@ -175,6 +192,20 @@ export function checkShape (acta) {
|
|
|
175
192
|
// `encpub`), y cuando no quede ninguno sin ella se aprieta aquí.
|
|
176
193
|
if (m.encPub != null && !isEncPub(m.encPub)) return 'encpub-invalido'
|
|
177
194
|
}
|
|
195
|
+
// LA LLAVE DE SELLADO Y SU REGISTRO. `sealPub` puede no estar (un perfil que no sella
|
|
196
|
+
// secretos), pero si está tiene que decir DESDE QUÉ acta manda: sin `sealSince` no se
|
|
197
|
+
// puede decidir con qué llave verificar un sobre viejo.
|
|
198
|
+
if (acta.v >= 2) {
|
|
199
|
+
if (acta.sealPub != null) {
|
|
200
|
+
if (!isPub(acta.sealPub)) return 'sealpub-invalido'
|
|
201
|
+
if (!Number.isInteger(acta.sealSince) || acta.sealSince < 1 || acta.sealSince > acta.seq) return 'sealsince'
|
|
202
|
+
} else if (acta.sealSince) return 'sealsince'
|
|
203
|
+
if (!Array.isArray(acta.sealKeys)) return 'sealkeys'
|
|
204
|
+
for (const k of acta.sealKeys) {
|
|
205
|
+
if (!isPub(k?.pub)) return 'sealkey-invalida'
|
|
206
|
+
if (!Number.isInteger(k.from) || !Number.isInteger(k.to) || k.from < 1 || k.to < k.from) return 'sealkey-rango'
|
|
207
|
+
}
|
|
208
|
+
}
|
|
178
209
|
if (new Set(acta.members.map((m) => m.pub)).size !== acta.members.length) return 'miembro-duplicado'
|
|
179
210
|
if (!acta.members.some((m) => m.pub === acta.sealer)) return 'sealer-no-es-miembro'
|
|
180
211
|
if (!acta.members.some((m) => m.caps.includes('sign'))) return 'sin-firmante'
|
|
@@ -216,7 +247,7 @@ export async function verifyActa ({ acta, expectedProfileId } = {}) {
|
|
|
216
247
|
* Van en ARRAY porque hay combinaciones que deben ser atómicas: admitir al nuevo sellador y
|
|
217
248
|
* traspasarle el master ocurre en el MISMO `seq` (§2.1.3).
|
|
218
249
|
*/
|
|
219
|
-
export async function applyChanges (acta, changes, { by, now = Date.now() } = {}) {
|
|
250
|
+
export async function applyChanges (acta, changes, { by, now = Date.now(), sealPub = null } = {}) {
|
|
220
251
|
const shape = checkShape(acta)
|
|
221
252
|
if (shape) throw new Error('invalid record: ' + shape)
|
|
222
253
|
if (!by) throw new Error('applyChanges: missing `by` (who seals)')
|
|
@@ -227,6 +258,11 @@ export async function applyChanges (acta, changes, { by, now = Date.now() } = {}
|
|
|
227
258
|
|
|
228
259
|
const next = {
|
|
229
260
|
...acta,
|
|
261
|
+
// Asciende de v1 a v2 sin ceremonia: los campos de sellado nacen vacíos y la llave
|
|
262
|
+
// entra en cuanto quien sella pase una (`sealPub`).
|
|
263
|
+
v: ACTA_V,
|
|
264
|
+
sealPub: acta.sealPub || null,
|
|
265
|
+
sealSince: acta.sealPub ? acta.sealSince : 0,
|
|
230
266
|
sealedBy: by,
|
|
231
267
|
seq: acta.seq + 1,
|
|
232
268
|
prev: await actaHash(acta),
|
|
@@ -234,10 +270,24 @@ export async function applyChanges (acta, changes, { by, now = Date.now() } = {}
|
|
|
234
270
|
revoked: [...(acta.revoked || [])],
|
|
235
271
|
renounced: [...(acta.renounced || [])],
|
|
236
272
|
keyring: (acta.keyring || []).map((g) => ({ ...g, wraps: { ...g.wraps } })),
|
|
273
|
+
sealKeys: (acta.sealKeys || []).map((k) => ({ ...k })),
|
|
237
274
|
updatedAt: now
|
|
238
275
|
}
|
|
239
276
|
delete next.sig
|
|
240
277
|
|
|
278
|
+
// LA LLAVE DE SELLADO ROTA CON EL ACTA (§8.9 de `secretos-sellados.md`). Quien sella
|
|
279
|
+
// pasa la llave nueva; si no pasa ninguna, la de antes sigue mandando —un master que no
|
|
280
|
+
// sella secretos (el navegador) no tiene por qué inventarse una—.
|
|
281
|
+
//
|
|
282
|
+
// La anterior NO se tira: se guarda con el tramo de `seq` en el que estuvo en vigor,
|
|
283
|
+
// porque los sobres que firmó tienen que seguir verificando. Re-firmarlos al rotar
|
|
284
|
+
// sería recorrer todos los secretos en cada cambio de membresía.
|
|
285
|
+
if (sealPub && sealPub !== next.sealPub) {
|
|
286
|
+
if (next.sealPub) next.sealKeys.push({ pub: next.sealPub, from: next.sealSince, to: acta.seq })
|
|
287
|
+
next.sealPub = sealPub
|
|
288
|
+
next.sealSince = next.seq
|
|
289
|
+
}
|
|
290
|
+
|
|
241
291
|
const find = (pub) => next.members.find((m) => m.pub === pub)
|
|
242
292
|
|
|
243
293
|
for (const ch of list) {
|
|
@@ -368,6 +418,19 @@ export async function applyChanges (acta, changes, { by, now = Date.now() } = {}
|
|
|
368
418
|
return next
|
|
369
419
|
}
|
|
370
420
|
|
|
421
|
+
/**
|
|
422
|
+
* La llave que firmaba los sobres cuando el acta iba por `seq`. Es lo que necesita quien
|
|
423
|
+
* VERIFICA un sobre: el sobre dice con qué acta se selló, y esto dice con qué llave hay
|
|
424
|
+
* que comprobarlo. `null` si no había ninguna (perfil que no sellaba) o si el `seq` viene
|
|
425
|
+
* del futuro, que es un sobre que no puede ser bueno.
|
|
426
|
+
*/
|
|
427
|
+
export function sealKeyAt (acta, seq) {
|
|
428
|
+
if (!acta || !Number.isInteger(seq) || seq < 1 || seq > acta.seq) return null
|
|
429
|
+
if (acta.sealPub && seq >= acta.sealSince) return acta.sealPub
|
|
430
|
+
for (const k of acta.sealKeys || []) if (seq >= k.from && seq <= k.to) return k.pub
|
|
431
|
+
return null
|
|
432
|
+
}
|
|
433
|
+
|
|
371
434
|
// ----- renuncia (§2.2): el único cambio que no pasa por el master -----
|
|
372
435
|
|
|
373
436
|
/** Crea el registro firmado con el que un miembro se QUITA capacidades a sí mismo. */
|
package/vault/core.js
CHANGED
|
@@ -542,6 +542,13 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
542
542
|
/** ¿Es ESTE dispositivo el master (el único que puede sellar)? */
|
|
543
543
|
const amMaster = () => loadActa()?.sealer === publickeyJwkStr
|
|
544
544
|
|
|
545
|
+
/**
|
|
546
|
+
* Quien sella SOBRES (la bóveda) pone aquí una función que estrena una llave de sellado
|
|
547
|
+
* y devuelve su pública. Se llama en cada acta, porque esa llave rota con el acta.
|
|
548
|
+
* Un navegador no sella secretos: se queda en `null` y el acta no lleva llave.
|
|
549
|
+
*/
|
|
550
|
+
let sealKeyProvider = null
|
|
551
|
+
|
|
545
552
|
/** Sella con la llave del perfil (CryptoKey, puede ser no extractable). */
|
|
546
553
|
const seal = (acta) => Acta.sealActa({ acta, privateKey: keypair.privateKey })
|
|
547
554
|
|
|
@@ -552,7 +559,15 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
552
559
|
async function sealChanges (changes) {
|
|
553
560
|
const acta = loadActa()
|
|
554
561
|
if (!acta) throw new Error('this profile has no record yet')
|
|
555
|
-
|
|
562
|
+
// LA LLAVE DE SELLADO ROTA CON EL ACTA (§8.9 de `secretos-sellados.md`): si quien usa
|
|
563
|
+
// esta identidad sella secretos —la bóveda—, le pedimos una llave nueva para nombrarla
|
|
564
|
+
// aquí. Si no hay proveedor, o falla, el acta sale igual con la llave de antes: no
|
|
565
|
+
// admitir un aparato porque no se pudo estrenar una llave sería el peor de los canjes.
|
|
566
|
+
let sealPub = null
|
|
567
|
+
if (sealKeyProvider) {
|
|
568
|
+
try { sealPub = await sealKeyProvider() } catch (_) { sealPub = null }
|
|
569
|
+
}
|
|
570
|
+
const next = await Acta.applyChanges(acta, changes, { by: publickeyJwkStr, sealPub })
|
|
556
571
|
const sealed = await seal(next)
|
|
557
572
|
pushHistory(acta) // la que deja de ser vigente entra en la ventana de retención
|
|
558
573
|
saveActa(sealed)
|
|
@@ -1447,6 +1462,23 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1447
1462
|
return { acta, isMaster: amMaster(), myCaps: Acta.effectiveCaps(acta, publickeyJwkStr, loadRenounces()) }
|
|
1448
1463
|
},
|
|
1449
1464
|
|
|
1465
|
+
/**
|
|
1466
|
+
* Quien sella sobres de secretos (la bóveda) registra aquí cómo estrenar su LLAVE DE
|
|
1467
|
+
* SELLADO: una función que crea el par, se guarda la privada y devuelve la pública. Se
|
|
1468
|
+
* llama al sellar cada acta, que es cuando esa llave rota (§8.9).
|
|
1469
|
+
*
|
|
1470
|
+
* Es opt-in a propósito: una identidad de navegador no sella nada, y un acta sin llave
|
|
1471
|
+
* de sellado es perfectamente válida.
|
|
1472
|
+
*/
|
|
1473
|
+
setSealKeyProvider (fn) {
|
|
1474
|
+
sealKeyProvider = typeof fn === 'function' ? fn : null
|
|
1475
|
+
},
|
|
1476
|
+
|
|
1477
|
+
/** La llave con la que se firmaron los sobres de un acta dada. Para VERIFICARLOS. */
|
|
1478
|
+
sealKeyAt (seq) {
|
|
1479
|
+
return Acta.sealKeyAt(loadActa(), seq)
|
|
1480
|
+
},
|
|
1481
|
+
|
|
1450
1482
|
async profileMembers () {
|
|
1451
1483
|
const acta = loadActa()
|
|
1452
1484
|
if (!acta) return { members: [], profileId: null, seq: 0, sealer: null }
|