@dotrino/identity 0.14.0 → 0.15.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 +18 -1
- package/src/node.js +16 -4
- package/vault/capabilities.js +44 -0
- package/vault/core.js +95 -1
- package/vault/peerStore.js +13 -23
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -259,6 +259,23 @@ export class Identity {
|
|
|
259
259
|
return this.on('vault', handler)
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
// ----- multi-perfil por dispositivo -----
|
|
263
|
+
// Podés tener varios perfiles (identidades) en el mismo navegador, cada uno conectado o no
|
|
264
|
+
// a su propio vault. Crear/cambiar setea el perfil activo; la app RECARGA la página y toma
|
|
265
|
+
// el nuevo (no reactivo: las apps abiertas conservan el perfil con el que cargaron).
|
|
266
|
+
/** Lista de perfiles: [{ id, name, pubkey, current }]. */
|
|
267
|
+
async listProfiles () { return this._call('listProfiles') }
|
|
268
|
+
/** El perfil activo: { id, name, pubkey }. */
|
|
269
|
+
async currentProfile () { return this._call('currentProfile') }
|
|
270
|
+
/** Crea un perfil nuevo (identidad fresca) y lo deja activo. La app debe recargar. */
|
|
271
|
+
async createProfile (name) { return this._call('createProfile', { name }) }
|
|
272
|
+
/** Cambia el perfil activo. La app debe recargar la página. */
|
|
273
|
+
async switchProfile (id) { return this._call('switchProfile', { id }) }
|
|
274
|
+
/** Renombra un perfil (o el activo si no se pasa id). */
|
|
275
|
+
async renameProfile (id, name) { return this._call('renameProfile', { id, name }) }
|
|
276
|
+
/** Borra un perfil y sus datos (no el único). */
|
|
277
|
+
async deleteProfile (id) { return this._call('deleteProfile', { id }) }
|
|
278
|
+
|
|
262
279
|
/**
|
|
263
280
|
* Merge endorsements (signed ratings from third parties) about a subject
|
|
264
281
|
* into the local peer book. Returns { merged, total }.
|
|
@@ -426,4 +443,4 @@ export class Identity {
|
|
|
426
443
|
|
|
427
444
|
// Helpers de capacidad SIN clave maestra (lado dispositivo + verificación), reutilizables
|
|
428
445
|
// por apps/bridges sin cargar el iframe del vault.
|
|
429
|
-
export { makeDeviceKey, signWithDevice, verifyDelegation, verifyChain, pubkeyId, deriveSAS, verifyDeviceSig, makePairingCode, commitCode, MAX_DELEGATION_MS, DEFAULT_DELEGATION_MS } from '../vault/capabilities.js'
|
|
446
|
+
export { makeDeviceKey, signWithDevice, verifyDelegation, verifyChain, pubkeyId, deriveSAS, verifyDeviceSig, makePairingCode, commitCode, avatarSvg, avatarDataUri, MAX_DELEGATION_MS, DEFAULT_DELEGATION_MS } from '../vault/capabilities.js'
|
package/src/node.js
CHANGED
|
@@ -41,14 +41,19 @@ function fileKv (filePath) {
|
|
|
41
41
|
function filePeers (filePath) {
|
|
42
42
|
let peers = {}
|
|
43
43
|
let markDirty = null
|
|
44
|
+
let pid = null // multi-perfil: el peer book se namespacea por perfil
|
|
45
|
+
const fileFor = () => pid ? path.join(path.dirname(filePath), `peers.${pid}.json`) : filePath
|
|
44
46
|
const flush = () => {
|
|
45
|
-
|
|
46
|
-
fs.
|
|
47
|
+
const f = fileFor()
|
|
48
|
+
fs.mkdirSync(path.dirname(f), { recursive: true })
|
|
49
|
+
fs.writeFileSync(f, JSON.stringify(peers))
|
|
47
50
|
}
|
|
48
51
|
return {
|
|
52
|
+
setProfile (p) { pid = p || null },
|
|
49
53
|
async initPeerStorage () {
|
|
54
|
+
const f = fileFor()
|
|
50
55
|
try {
|
|
51
|
-
|
|
56
|
+
peers = (fs.existsSync(f)) ? (JSON.parse(fs.readFileSync(f, 'utf8')) || {}) : {}
|
|
52
57
|
} catch (_) { peers = {} }
|
|
53
58
|
return peers
|
|
54
59
|
},
|
|
@@ -144,6 +149,13 @@ export class Identity {
|
|
|
144
149
|
listVaultDevices () { return this._h('listVaultDevices') }
|
|
145
150
|
getVaultCert () { return this._h('getVaultCert') }
|
|
146
151
|
onVault (handler) { return this.on('vault', handler) }
|
|
152
|
+
// Multi-perfil por dispositivo (crear/cambiar reinicializa con el nuevo perfil activo).
|
|
153
|
+
listProfiles () { return this._h('listProfiles') }
|
|
154
|
+
currentProfile () { return this._h('currentProfile') }
|
|
155
|
+
createProfile (name) { return this._h('createProfile', { name }) }
|
|
156
|
+
switchProfile (id) { return this._h('switchProfile', { id }) }
|
|
157
|
+
renameProfile (id, name) { return this._h('renameProfile', { id, name }) }
|
|
158
|
+
deleteProfile (id) { return this._h('deleteProfile', { id }) }
|
|
147
159
|
mergeEndorsements (subject, endorsements, askerPubkey) {
|
|
148
160
|
return this._h('mergeEndorsements', { subject, endorsements, askerPubkey })
|
|
149
161
|
}
|
|
@@ -186,4 +198,4 @@ export default Identity
|
|
|
186
198
|
|
|
187
199
|
// Helpers de capacidad SIN clave maestra (lado dispositivo + verificación), para que
|
|
188
200
|
// un bridge/bot Node pueda crear su clave, firmar acciones y verificar cadenas D←P.
|
|
189
|
-
export { makeDeviceKey, signWithDevice, verifyDelegation, verifyChain, pubkeyId, deriveSAS, verifyDeviceSig, makePairingCode, commitCode, MAX_DELEGATION_MS, DEFAULT_DELEGATION_MS } from '../vault/capabilities.js'
|
|
201
|
+
export { makeDeviceKey, signWithDevice, verifyDelegation, verifyChain, pubkeyId, deriveSAS, verifyDeviceSig, makePairingCode, commitCode, avatarSvg, avatarDataUri, MAX_DELEGATION_MS, DEFAULT_DELEGATION_MS } from '../vault/capabilities.js'
|
package/vault/capabilities.js
CHANGED
|
@@ -99,6 +99,50 @@ export async function commitCode ({ code, dpub, sn }) {
|
|
|
99
99
|
return [...new Uint8Array(h)].map((x) => x.toString(16).padStart(2, '0')).join('')
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Avatar generado (identicon de Dotrino): SVG DETERMINISTA a partir de una semilla
|
|
104
|
+
* (típicamente el pubkey del perfil) → cada perfil/identidad nace con imagen, sin que el
|
|
105
|
+
* usuario tenga que subir nada. Síncrono (hash FNV-1a + xorshift, sin necesidades de
|
|
106
|
+
* seguridad: es solo decorativo) → usable directo en plantillas. Rejilla 5×5 simétrica
|
|
107
|
+
* sobre una "moneda" redondeada, con color derivado del hash.
|
|
108
|
+
*/
|
|
109
|
+
function _hashSeed (s) {
|
|
110
|
+
let h = 2166136261 >>> 0
|
|
111
|
+
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619) }
|
|
112
|
+
const bytes = []
|
|
113
|
+
let x = (h ^ 0x9e3779b9) >>> 0
|
|
114
|
+
for (let i = 0; i < 16; i++) { x ^= x << 13; x ^= x >>> 17; x ^= x << 5; x >>>= 0; bytes.push(x & 0xff) }
|
|
115
|
+
return { h: h >>> 0, bytes }
|
|
116
|
+
}
|
|
117
|
+
export function avatarSvg (seed, { size = 80 } = {}) {
|
|
118
|
+
const { h, bytes } = _hashSeed(String(seed || 'dotrino'))
|
|
119
|
+
const hue = h % 360
|
|
120
|
+
const hue2 = (hue + 40) % 360
|
|
121
|
+
const fg = `hsl(${hue} 62% 46%)`
|
|
122
|
+
const bg1 = `hsl(${hue} 48% 95%)`
|
|
123
|
+
const bg2 = `hsl(${hue2} 48% 90%)`
|
|
124
|
+
const cells = 5
|
|
125
|
+
const unit = size / cells
|
|
126
|
+
let rects = ''
|
|
127
|
+
for (let col = 0; col < 3; col++) {
|
|
128
|
+
for (let row = 0; row < cells; row++) {
|
|
129
|
+
if (!(bytes[col * cells + row] & 1)) continue
|
|
130
|
+
for (const c of (col === 2 ? [2] : [col, cells - 1 - col])) {
|
|
131
|
+
rects += `<rect x="${(c * unit).toFixed(2)}" y="${(row * unit).toFixed(2)}" width="${unit.toFixed(2)}" height="${unit.toFixed(2)}"/>`
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const id = 'g' + (h % 100000)
|
|
136
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">` +
|
|
137
|
+
`<defs><linearGradient id="${id}" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="${bg1}"/><stop offset="1" stop-color="${bg2}"/></linearGradient></defs>` +
|
|
138
|
+
`<rect width="${size}" height="${size}" rx="${(size * 0.5).toFixed(2)}" fill="url(#${id})"/>` +
|
|
139
|
+
`<g fill="${fg}" transform="translate(${(size * 0.12).toFixed(2)} ${(size * 0.12).toFixed(2)}) scale(0.76)">${rects}</g></svg>`
|
|
140
|
+
}
|
|
141
|
+
/** El avatar como data-URI listo para `<img src>` o `background-image`. */
|
|
142
|
+
export function avatarDataUri (seed, opts) {
|
|
143
|
+
return 'data:image/svg+xml,' + encodeURIComponent(avatarSvg(seed, opts))
|
|
144
|
+
}
|
|
145
|
+
|
|
102
146
|
/** Cuerpo canónico del certificado (lo que se firma): el cert SIN la firma. */
|
|
103
147
|
export function delegationBody (cert) {
|
|
104
148
|
return { v: cert.v, iss: cert.iss, sub: cert.sub, scope: cert.scope, iat: cert.iat, exp: cert.exp, nonce: cert.nonce }
|
package/vault/core.js
CHANGED
|
@@ -29,6 +29,10 @@ export const DELEGATIONS_STORAGE = 'dotrino.identity.delegations' // caps emit
|
|
|
29
29
|
export const REVOCATIONS_STORAGE = 'dotrino.identity.revocations' // nonces revocados
|
|
30
30
|
export const VAULT_DEVICE_STORAGE = 'dotrino.identity.vault.device' // sub-clave D de ESTE dispositivo (custodia en el iframe)
|
|
31
31
|
export const VAULT_CERT_STORAGE = 'dotrino.identity.vault.cert' // { cert, master, proxy, deviceId, pairedAt }
|
|
32
|
+
// Multi-perfil por dispositivo: lista de perfiles + el activo. Cada perfil tiene su propio
|
|
33
|
+
// namespace `dotrino.identity.p.<id>.<suffix>` para TODAS las claves de arriba (keypair, me, etc.).
|
|
34
|
+
export const PROFILES_STORAGE = 'dotrino.identity.profiles' // [{ id, name, pubkey }]
|
|
35
|
+
export const CURRENT_STORAGE = 'dotrino.identity.current' // id del perfil activo
|
|
32
36
|
|
|
33
37
|
const NONCE_TTL_MS = 5 * 60 * 1000
|
|
34
38
|
|
|
@@ -104,11 +108,27 @@ async function verifyBytes (publicJwkStr, bytes, signatureBase64) {
|
|
|
104
108
|
* @returns {Promise<{ handlers:Object, get me():Object, sync:Object|null,
|
|
105
109
|
* onSyncStatus(fn):void }>}
|
|
106
110
|
*/
|
|
107
|
-
export async function createIdentityCore ({ kv, peers, makeSync = null }) {
|
|
111
|
+
export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null }) {
|
|
108
112
|
const {
|
|
109
113
|
initPeerStorage, loadPeers, savePeers, setPeersDirect, upsertPeer, onDirty
|
|
110
114
|
} = peers
|
|
111
115
|
|
|
116
|
+
// ----- multi-perfil: kv SCOPEADO por el perfil activo -----
|
|
117
|
+
// Todas las claves `dotrino.identity.*` (keypair, me, nonces, delegations, vault.*) se
|
|
118
|
+
// namespacean transparentemente bajo `dotrino.identity.p.<currentPid>.*`. Las dos claves
|
|
119
|
+
// globales (lista de perfiles + activo) usan el kv crudo. Cambiar de perfil = setear el
|
|
120
|
+
// activo; la app recarga la página y re-inicializa con el nuevo (no reactivo, por diseño).
|
|
121
|
+
let currentPid = null
|
|
122
|
+
const _scoped = (k) => (!currentPid || k === PROFILES_STORAGE || k === CURRENT_STORAGE)
|
|
123
|
+
? k : k.replace(/^dotrino\.identity\./, `dotrino.identity.p.${currentPid}.`)
|
|
124
|
+
const kv = {
|
|
125
|
+
getItem: (k) => rawKv.getItem(_scoped(k)),
|
|
126
|
+
setItem: (k, v) => rawKv.setItem(_scoped(k), v),
|
|
127
|
+
removeItem: (k) => rawKv.removeItem(_scoped(k))
|
|
128
|
+
}
|
|
129
|
+
const loadProfiles = () => { try { return JSON.parse(rawKv.getItem(PROFILES_STORAGE) || '[]') || [] } catch { return [] } }
|
|
130
|
+
const saveProfiles = (list) => rawKv.setItem(PROFILES_STORAGE, JSON.stringify(list))
|
|
131
|
+
|
|
112
132
|
// ----- keypair loaders (kv-backed) -----
|
|
113
133
|
|
|
114
134
|
async function loadOrCreateKeypair () {
|
|
@@ -511,6 +531,54 @@ export async function createIdentityCore ({ kv, peers, makeSync = null }) {
|
|
|
511
531
|
}
|
|
512
532
|
},
|
|
513
533
|
|
|
534
|
+
// ----- perfiles (multi-perfil por dispositivo) -----
|
|
535
|
+
// Cambiar/crear setea el perfil activo; la app RECARGA la página y re-inicializa con él
|
|
536
|
+
// (no reactivo, por diseño). Las apps abiertas conservan el perfil con el que cargaron.
|
|
537
|
+
async listProfiles () {
|
|
538
|
+
return loadProfiles().map((p) => ({ id: p.id, name: p.name || '', pubkey: p.pubkey || null, current: p.id === currentPid }))
|
|
539
|
+
},
|
|
540
|
+
async currentProfile () {
|
|
541
|
+
const e = loadProfiles().find((p) => p.id === currentPid) || {}
|
|
542
|
+
return { id: currentPid, name: e.name || me?.nickname || '', pubkey: publickeyJwkStr }
|
|
543
|
+
},
|
|
544
|
+
async createProfile ({ name } = {}) {
|
|
545
|
+
const pid = 'p' + crypto.randomUUID().slice(0, 8)
|
|
546
|
+
currentPid = pid
|
|
547
|
+
rawKv.setItem(CURRENT_STORAGE, pid)
|
|
548
|
+
await peers.setProfile?.(pid)
|
|
549
|
+
await initPeerStorage()
|
|
550
|
+
keypair = await loadOrCreateKeypair(); publickeyJwkStr = JSON.stringify(keypair.publicJwk)
|
|
551
|
+
encKeypair = await loadOrCreateEncKeypair(); encPublickeyJwkStr = JSON.stringify(encKeypair.publicJwk)
|
|
552
|
+
me = { publickey: publickeyJwkStr, encryptionPubkey: encPublickeyJwkStr, nickname: String(name || '').slice(0, 40) }
|
|
553
|
+
saveMe(me)
|
|
554
|
+
const list = loadProfiles(); list.push({ id: pid, name: me.nickname, pubkey: publickeyJwkStr }); saveProfiles(list)
|
|
555
|
+
return { id: pid, name: me.nickname, pubkey: publickeyJwkStr }
|
|
556
|
+
},
|
|
557
|
+
async switchProfile ({ id } = {}) {
|
|
558
|
+
if (!loadProfiles().find((p) => p.id === id)) throw new Error('perfil no existe')
|
|
559
|
+
rawKv.setItem(CURRENT_STORAGE, id) // la app recarga la página → re-init con el nuevo perfil
|
|
560
|
+
return { id }
|
|
561
|
+
},
|
|
562
|
+
async renameProfile ({ id, name } = {}) {
|
|
563
|
+
const list = loadProfiles(); const e = list.find((p) => p.id === (id || currentPid))
|
|
564
|
+
if (!e) throw new Error('perfil no existe')
|
|
565
|
+
e.name = String(name || '').slice(0, 40); saveProfiles(list)
|
|
566
|
+
if (e.id === currentPid) { me = { ...(me || {}), nickname: e.name }; saveMe(me) }
|
|
567
|
+
return { id: e.id, name: e.name }
|
|
568
|
+
},
|
|
569
|
+
async deleteProfile ({ id } = {}) {
|
|
570
|
+
let list = loadProfiles()
|
|
571
|
+
if (list.length <= 1) throw new Error('no se puede borrar el único perfil')
|
|
572
|
+
if (!list.find((p) => p.id === id)) throw new Error('perfil no existe')
|
|
573
|
+
list = list.filter((p) => p.id !== id); saveProfiles(list)
|
|
574
|
+
// Borrado directo del namespace del perfil (incluye su store del vault si lo tuviera).
|
|
575
|
+
for (const s of ['keypair', 'enc-keypair', 'me', 'nonces', 'delegations', 'revocations', 'vault.device', 'vault.cert']) {
|
|
576
|
+
rawKv.removeItem(`dotrino.identity.p.${id}.${s}`)
|
|
577
|
+
}
|
|
578
|
+
if (currentPid === id) { currentPid = list[0].id; rawKv.setItem(CURRENT_STORAGE, currentPid) }
|
|
579
|
+
return { ok: true, current: currentPid }
|
|
580
|
+
},
|
|
581
|
+
|
|
514
582
|
// ----- emparejar ESTE dispositivo con el vault del usuario (Fase 1) -----
|
|
515
583
|
// Genera D aquí dentro (su privada NUNCA sale de la identidad), hace el enroll
|
|
516
584
|
// endurecido por el proxy y guarda el cert. NO cambia signData todavía (Fase 2).
|
|
@@ -671,6 +739,24 @@ export async function createIdentityCore ({ kv, peers, makeSync = null }) {
|
|
|
671
739
|
|
|
672
740
|
// ----- bootstrap -----
|
|
673
741
|
|
|
742
|
+
// Perfil activo (multi-perfil por dispositivo). Sin migración (ecosistema nuevo, no importa
|
|
743
|
+
// perder data): si no hay perfiles, se crea uno fresco. A partir de acá `kv` está scopeado a
|
|
744
|
+
// `currentPid` y `peers` apunta al peer book de ese perfil.
|
|
745
|
+
{
|
|
746
|
+
let profiles = loadProfiles()
|
|
747
|
+
currentPid = rawKv.getItem(CURRENT_STORAGE)
|
|
748
|
+
if (!profiles.length) {
|
|
749
|
+
currentPid = 'p' + crypto.randomUUID().slice(0, 8)
|
|
750
|
+
profiles = [{ id: currentPid, name: '', pubkey: null }]
|
|
751
|
+
saveProfiles(profiles)
|
|
752
|
+
rawKv.setItem(CURRENT_STORAGE, currentPid)
|
|
753
|
+
} else if (!currentPid || !profiles.find((p) => p.id === currentPid)) {
|
|
754
|
+
currentPid = profiles[0].id
|
|
755
|
+
rawKv.setItem(CURRENT_STORAGE, currentPid)
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
await peers.setProfile?.(currentPid)
|
|
759
|
+
|
|
674
760
|
keypair = await loadOrCreateKeypair()
|
|
675
761
|
publickeyJwkStr = JSON.stringify(keypair.publicJwk)
|
|
676
762
|
encKeypair = await loadOrCreateEncKeypair()
|
|
@@ -690,6 +776,14 @@ export async function createIdentityCore ({ kv, peers, makeSync = null }) {
|
|
|
690
776
|
kv.setItem(ME_STORAGE, JSON.stringify(me))
|
|
691
777
|
}
|
|
692
778
|
|
|
779
|
+
// Registrar el pubkey (y nombre) del perfil activo en su meta → para avatar/listado sin abrir cada perfil.
|
|
780
|
+
{
|
|
781
|
+
const list = loadProfiles(); const e = list.find((p) => p.id === currentPid)
|
|
782
|
+
if (e && (e.pubkey !== publickeyJwkStr || (!e.name && me?.nickname))) {
|
|
783
|
+
e.pubkey = publickeyJwkStr; if (!e.name && me?.nickname) e.name = me.nickname; saveProfiles(list)
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
693
787
|
if (typeof makeSync === 'function') {
|
|
694
788
|
sync = makeSync({
|
|
695
789
|
fileName: 'dotrino-identity-backup.json',
|
package/vault/peerStore.js
CHANGED
|
@@ -31,10 +31,15 @@ let _fallback = false
|
|
|
31
31
|
let _idb = null
|
|
32
32
|
let _writeChain = Promise.resolve()
|
|
33
33
|
let _markDirty = null
|
|
34
|
+
let _pid = null
|
|
34
35
|
|
|
35
36
|
/** Registra el callback que marca el estado como "sucio" para el sync. */
|
|
36
37
|
export function onDirty (fn) { _markDirty = fn }
|
|
37
38
|
|
|
39
|
+
/** Multi-perfil: namespacea el peer book por perfil. El core lo llama antes de initPeerStorage. */
|
|
40
|
+
export function setProfile (pid) { _pid = pid || null }
|
|
41
|
+
function peersKey () { return _pid ? `peers.${_pid}.v1` : IDB_PEERS_KEY }
|
|
42
|
+
|
|
38
43
|
function openIdb () {
|
|
39
44
|
return new Promise((resolve, reject) => {
|
|
40
45
|
let req
|
|
@@ -74,43 +79,28 @@ export async function initPeerStorage () {
|
|
|
74
79
|
catch (_) { /* best-effort */ }
|
|
75
80
|
try {
|
|
76
81
|
_idb = await openIdb()
|
|
77
|
-
const stored = await idbGet(_idb,
|
|
78
|
-
|
|
79
|
-
const migratedFlag = await idbGet(_idb, IDB_MIGRATED_KEY)
|
|
80
|
-
if (migratedFlag) {
|
|
81
|
-
// Ya reconciliado: IndexedDB es la fuente de verdad (ignora el LS viejo).
|
|
82
|
-
_peers = storedPeers
|
|
83
|
-
} else {
|
|
84
|
-
// Primera corrida (o reintento tras el bug que escribía {}): unimos el
|
|
85
|
-
// peer book del localStorage viejo con lo que haya en IndexedDB —unión,
|
|
86
|
-
// IndexedDB gana en conflictos— para recuperar contactos que un bug previo
|
|
87
|
-
// pudo enmascarar. Nunca borra. Marcamos el flag para no rehacerlo (así no
|
|
88
|
-
// se "resucitan" contactos que el usuario borre más adelante).
|
|
89
|
-
const local = readLocalPeers()
|
|
90
|
-
_peers = { ...local, ...storedPeers }
|
|
91
|
-
const recovered = Object.keys(local).filter(k => !(k in storedPeers)).length
|
|
92
|
-
await idbPut(_idb, IDB_PEERS_KEY, _peers)
|
|
93
|
-
await idbPut(_idb, IDB_MIGRATED_KEY, true)
|
|
94
|
-
if (recovered) console.log(`[cc-identity] ${recovered} peer(s) recuperados del localStorage viejo → IndexedDB`)
|
|
95
|
-
}
|
|
82
|
+
const stored = await idbGet(_idb, peersKey()) // peer book DEL perfil activo (namespaceado)
|
|
83
|
+
_peers = (stored && typeof stored === 'object') ? stored : {}
|
|
96
84
|
} catch (e) {
|
|
97
85
|
console.warn('[cc-identity] IndexedDB no disponible, uso localStorage:', e?.message)
|
|
98
86
|
_fallback = true
|
|
99
87
|
_idb = null
|
|
100
|
-
_peers =
|
|
88
|
+
try { const raw = localStorage.getItem(peersKey()); _peers = raw ? (JSON.parse(raw) || {}) : {} }
|
|
89
|
+
catch (_) { _peers = {} }
|
|
101
90
|
}
|
|
102
91
|
return _peers
|
|
103
92
|
}
|
|
104
93
|
|
|
105
94
|
function persistPeers () {
|
|
95
|
+
const key = peersKey()
|
|
106
96
|
if (_fallback || !_idb) {
|
|
107
|
-
try { localStorage.setItem(
|
|
97
|
+
try { localStorage.setItem(key, JSON.stringify(_peers)) }
|
|
108
98
|
catch (e) { console.warn('[cc-identity] persist (ls) falló:', e?.message) }
|
|
109
99
|
return _writeChain
|
|
110
100
|
}
|
|
111
101
|
const snapshot = _peers
|
|
112
102
|
_writeChain = _writeChain
|
|
113
|
-
.then(() => idbPut(_idb,
|
|
103
|
+
.then(() => idbPut(_idb, key, snapshot))
|
|
114
104
|
.catch(e => console.warn('[cc-identity] persist (idb) falló:', e?.message))
|
|
115
105
|
return _writeChain
|
|
116
106
|
}
|
|
@@ -143,5 +133,5 @@ export function upsertPeer (publickey, patch) {
|
|
|
143
133
|
// Sólo para tests: resetea el estado del módulo (y cierra la conexión IDB).
|
|
144
134
|
export function _resetForTest () {
|
|
145
135
|
try { if (_idb && _idb.close) _idb.close() } catch (_) {}
|
|
146
|
-
_peers = {}; _fallback = false; _idb = null; _writeChain = Promise.resolve(); _markDirty = null
|
|
136
|
+
_peers = {}; _fallback = false; _idb = null; _writeChain = Promise.resolve(); _markDirty = null; _pid = null
|
|
147
137
|
}
|