@dotrino/identity 0.28.0 → 0.29.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/core.js +50 -2
- package/vault/remote.js +4 -3
package/package.json
CHANGED
package/vault/core.js
CHANGED
|
@@ -32,6 +32,7 @@ export const REVOCATIONS_STORAGE = 'dotrino.identity.revocations' // nonces re
|
|
|
32
32
|
export const VAULT_DEVICE_STORAGE = 'dotrino.identity.vault.device' // sub-clave D de ESTE dispositivo (custodia en el iframe)
|
|
33
33
|
export const VAULT_CERT_STORAGE = 'dotrino.identity.vault.cert' // { cert, master, proxy, deviceId, pairedAt }
|
|
34
34
|
export const ACTA_STORAGE = 'dotrino.identity.acta' // acta de perfil vigente (quién es del perfil y qué puede)
|
|
35
|
+
export const ACTA_HISTORY_STORAGE = 'dotrino.identity.acta.history' // últimas actas selladas (§1.3)
|
|
35
36
|
export const RENOUNCE_STORAGE = 'dotrino.identity.renounced' // renuncias propias aún no absorbidas por el master
|
|
36
37
|
// Multi-perfil por dispositivo: lista de perfiles + el activo. Cada perfil tiene su propio
|
|
37
38
|
// namespace `dotrino.identity.p.<id>.<suffix>` para TODAS las claves de arriba (keypair, me, etc.).
|
|
@@ -345,6 +346,20 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
345
346
|
// `acta.js`, que es puro y está probado aparte.
|
|
346
347
|
const loadActa = () => { try { return JSON.parse(kv.getItem(ACTA_STORAGE) || 'null') } catch (_) { return null } }
|
|
347
348
|
const saveActa = (a) => kv.setItem(ACTA_STORAGE, JSON.stringify(a))
|
|
349
|
+
// VENTANA DE RETENCIÓN (§1.3): el master conserva las últimas actas para que un miembro
|
|
350
|
+
// que estuvo apagado pueda comprobar el encadenamiento al volver. Un tercero no las
|
|
351
|
+
// necesita —le basta el snapshot actual—, pero entre miembros hay que poder verificar
|
|
352
|
+
// que la nueva desciende de la que uno tenía. Más viejo que la ventana ⇒ re-admitirse.
|
|
353
|
+
const ACTA_WINDOW = 50
|
|
354
|
+
const loadHistory = () => { try { return JSON.parse(kv.getItem(ACTA_HISTORY_STORAGE) || '[]') || [] } catch (_) { return [] } }
|
|
355
|
+
const pushHistory = (acta) => {
|
|
356
|
+
if (!acta) return
|
|
357
|
+
const h = loadHistory().filter((a) => a.seq !== acta.seq)
|
|
358
|
+
h.push(acta)
|
|
359
|
+
h.sort((a, b) => a.seq - b.seq)
|
|
360
|
+
kv.setItem(ACTA_HISTORY_STORAGE, JSON.stringify(h.slice(-ACTA_WINDOW)))
|
|
361
|
+
}
|
|
362
|
+
|
|
348
363
|
const loadRenounces = () => { try { return JSON.parse(kv.getItem(RENOUNCE_STORAGE) || '[]') || [] } catch (_) { return [] } }
|
|
349
364
|
const saveRenounces = (l) => kv.setItem(RENOUNCE_STORAGE, JSON.stringify(l))
|
|
350
365
|
|
|
@@ -363,6 +378,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
363
378
|
if (!acta) throw new Error('este perfil todavía no tiene acta')
|
|
364
379
|
const next = await Acta.applyChanges(acta, changes, { by: publickeyJwkStr })
|
|
365
380
|
const sealed = await seal(next)
|
|
381
|
+
pushHistory(acta) // la que deja de ser vigente entra en la ventana de retención
|
|
366
382
|
saveActa(sealed)
|
|
367
383
|
emitVault({ phase: 'acta', seq: sealed.seq, sealer: sealed.sealer })
|
|
368
384
|
return sealed
|
|
@@ -447,6 +463,20 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
447
463
|
* Adopta un acta que llega de otro miembro, si gana según §2.4.1 (seq mayor que encadene,
|
|
448
464
|
* o el traspaso a igual seq). Nunca retrocede.
|
|
449
465
|
*/
|
|
466
|
+
/**
|
|
467
|
+
* Adopta una CADENA de actas, una a una. Es lo que permite ponerse al día tras estar
|
|
468
|
+
* apagado sin bajar la guardia: cada eslabón se comprueba contra el anterior en vez de
|
|
469
|
+
* aceptar un salto a ciegas.
|
|
470
|
+
*/
|
|
471
|
+
async function adoptChain (chain) {
|
|
472
|
+
let last = null
|
|
473
|
+
for (const a of [...(chain || [])].sort((x, y) => x.seq - y.seq)) {
|
|
474
|
+
const r = await adoptActa(a)
|
|
475
|
+
if (r.adopted) last = r
|
|
476
|
+
}
|
|
477
|
+
return last || { adopted: false, reason: 'nada-que-adoptar', seq: loadActa()?.seq ?? null }
|
|
478
|
+
}
|
|
479
|
+
|
|
450
480
|
async function adoptActa (candidate) {
|
|
451
481
|
const current = loadActa()
|
|
452
482
|
const r = await Acta.canAdopt({ candidate, current })
|
|
@@ -1162,9 +1192,23 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1162
1192
|
return { ok: true, ...r }
|
|
1163
1193
|
},
|
|
1164
1194
|
|
|
1195
|
+
/**
|
|
1196
|
+
* Las actas que este master conserva desde `sinceSeq` (sin incluirla), para que un
|
|
1197
|
+
* miembro que volvió pueda comprobar el encadenamiento. Vacío si se salió de la ventana.
|
|
1198
|
+
*/
|
|
1199
|
+
async actaHistory ({ sinceSeq = 0 } = {}) {
|
|
1200
|
+
const cur = loadActa()
|
|
1201
|
+
const hist = loadHistory().filter((a) => a.seq > sinceSeq)
|
|
1202
|
+
const all = cur && cur.seq > sinceSeq ? [...hist, cur] : hist
|
|
1203
|
+
return { chain: all.sort((a, b) => a.seq - b.seq), window: ACTA_WINDOW }
|
|
1204
|
+
},
|
|
1205
|
+
|
|
1165
1206
|
/** Adopta un acta que llega de otro miembro (gana el seq mayor; a igual seq, el traspaso). */
|
|
1166
1207
|
async adoptActa ({ acta } = {}) { return adoptActa(acta) },
|
|
1167
1208
|
|
|
1209
|
+
/** Adopta una cadena completa (para ponerse al día tras estar apagado). */
|
|
1210
|
+
async adoptActaChain ({ chain } = {}) { return adoptChain(chain) },
|
|
1211
|
+
|
|
1168
1212
|
/** Une este dispositivo al perfil de otra bóveda (solo si aquí no hay nada que perder). */
|
|
1169
1213
|
async joinProfile ({ acta } = {}) { return joinProfile(acta) },
|
|
1170
1214
|
|
|
@@ -1253,9 +1297,13 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1253
1297
|
if (!v?.cert || !device) throw new Error('este dispositivo no está emparejado con un vault')
|
|
1254
1298
|
maybeRenewVaultCert()
|
|
1255
1299
|
try {
|
|
1256
|
-
const res = await remoteDevices({ master: v.master, proxy: v.proxy, device, cert: v.cert, onRevoked: wipeVaultLink })
|
|
1300
|
+
const res = await remoteDevices({ master: v.master, proxy: v.proxy, device, cert: v.cert, sinceSeq: loadActa()?.seq ?? 0, onRevoked: wipeVaultLink })
|
|
1257
1301
|
// El acta viaja con la lista: así los cambios de política llegan sin canal aparte.
|
|
1258
|
-
|
|
1302
|
+
// Si estuve apagado, viene la CADENA y se adopta eslabón a eslabón (§1.3).
|
|
1303
|
+
try {
|
|
1304
|
+
if (res.chain?.length && res.chain[0].profileId === loadActa()?.profileId) await adoptChain(res.chain)
|
|
1305
|
+
else if (res.acta) await (res.acta.profileId === loadActa()?.profileId ? adoptActa(res.acta) : joinProfile(res.acta))
|
|
1306
|
+
} catch (_) {}
|
|
1259
1307
|
return res
|
|
1260
1308
|
} catch (e) { return handleVaultError(e) }
|
|
1261
1309
|
},
|
package/vault/remote.js
CHANGED
|
@@ -166,9 +166,10 @@ export async function requestStore ({ master, proxy, device, cert, method, args,
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/** Lista (solo lectura) los dispositivos enrolados en tu vault. */
|
|
169
|
-
export async function requestDevices ({ master, proxy, device, cert, onRevoked } = {}) {
|
|
170
|
-
const
|
|
171
|
-
|
|
169
|
+
export async function requestDevices ({ master, proxy, device, cert, sinceSeq, onRevoked } = {}) {
|
|
170
|
+
const data = typeof sinceSeq === 'number' ? { op: 'devices', sinceSeq } : { op: 'devices' }
|
|
171
|
+
const res = await vaultRpc({ master, proxy, device, cert, onRevoked, sendType: 'vault.devices', okType: 'vault.devices.result', data })
|
|
172
|
+
return { devices: res.devices || [], revoked: res.revoked || [], acta: res.acta || null, chain: res.chain || null }
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
/**
|