@dotrino/identity 0.14.0 → 0.16.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 +111 -4
- 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 () {
|
|
@@ -175,6 +195,16 @@ export async function createIdentityCore ({ kv, peers, makeSync = null }) {
|
|
|
175
195
|
// Canal de eventos 'vault' (p.ej. el SAS a comparar durante el emparejamiento).
|
|
176
196
|
const vaultListeners = new Set()
|
|
177
197
|
const emitVault = (p) => { for (const fn of vaultListeners) { try { fn(p) } catch (_) {} } }
|
|
198
|
+
// Si el vault RECHAZA por cert revocado, este dispositivo perdió el acceso: limpiamos el
|
|
199
|
+
// cert local (ya no sirve) y emitimos 'revoked' → @dotrino/store borra SOLO el store de
|
|
200
|
+
// ESTE perfil (los demás perfiles quedan intactos). Cualquier otro error se propaga igual.
|
|
201
|
+
const handleVaultError = (e) => {
|
|
202
|
+
if (e && /\brevoked\b/.test(e.message || '')) {
|
|
203
|
+
try { kv.removeItem(VAULT_CERT_STORAGE); kv.removeItem(VAULT_DEVICE_STORAGE) } catch (_) {}
|
|
204
|
+
emitVault({ phase: 'revoked' })
|
|
205
|
+
}
|
|
206
|
+
throw e
|
|
207
|
+
}
|
|
178
208
|
const loadVaultCert = () => { try { return JSON.parse(kv.getItem(VAULT_CERT_STORAGE) || 'null') } catch (_) { return null } }
|
|
179
209
|
const loadVaultDevice = () => { try { return JSON.parse(kv.getItem(VAULT_DEVICE_STORAGE) || 'null') } catch (_) { return null } }
|
|
180
210
|
|
|
@@ -511,6 +541,54 @@ export async function createIdentityCore ({ kv, peers, makeSync = null }) {
|
|
|
511
541
|
}
|
|
512
542
|
},
|
|
513
543
|
|
|
544
|
+
// ----- perfiles (multi-perfil por dispositivo) -----
|
|
545
|
+
// Cambiar/crear setea el perfil activo; la app RECARGA la página y re-inicializa con él
|
|
546
|
+
// (no reactivo, por diseño). Las apps abiertas conservan el perfil con el que cargaron.
|
|
547
|
+
async listProfiles () {
|
|
548
|
+
return loadProfiles().map((p) => ({ id: p.id, name: p.name || '', pubkey: p.pubkey || null, current: p.id === currentPid }))
|
|
549
|
+
},
|
|
550
|
+
async currentProfile () {
|
|
551
|
+
const e = loadProfiles().find((p) => p.id === currentPid) || {}
|
|
552
|
+
return { id: currentPid, name: e.name || me?.nickname || '', pubkey: publickeyJwkStr }
|
|
553
|
+
},
|
|
554
|
+
async createProfile ({ name } = {}) {
|
|
555
|
+
const pid = 'p' + crypto.randomUUID().slice(0, 8)
|
|
556
|
+
currentPid = pid
|
|
557
|
+
rawKv.setItem(CURRENT_STORAGE, pid)
|
|
558
|
+
await peers.setProfile?.(pid)
|
|
559
|
+
await initPeerStorage()
|
|
560
|
+
keypair = await loadOrCreateKeypair(); publickeyJwkStr = JSON.stringify(keypair.publicJwk)
|
|
561
|
+
encKeypair = await loadOrCreateEncKeypair(); encPublickeyJwkStr = JSON.stringify(encKeypair.publicJwk)
|
|
562
|
+
me = { publickey: publickeyJwkStr, encryptionPubkey: encPublickeyJwkStr, nickname: String(name || '').slice(0, 40) }
|
|
563
|
+
saveMe(me)
|
|
564
|
+
const list = loadProfiles(); list.push({ id: pid, name: me.nickname, pubkey: publickeyJwkStr }); saveProfiles(list)
|
|
565
|
+
return { id: pid, name: me.nickname, pubkey: publickeyJwkStr }
|
|
566
|
+
},
|
|
567
|
+
async switchProfile ({ id } = {}) {
|
|
568
|
+
if (!loadProfiles().find((p) => p.id === id)) throw new Error('perfil no existe')
|
|
569
|
+
rawKv.setItem(CURRENT_STORAGE, id) // la app recarga la página → re-init con el nuevo perfil
|
|
570
|
+
return { id }
|
|
571
|
+
},
|
|
572
|
+
async renameProfile ({ id, name } = {}) {
|
|
573
|
+
const list = loadProfiles(); const e = list.find((p) => p.id === (id || currentPid))
|
|
574
|
+
if (!e) throw new Error('perfil no existe')
|
|
575
|
+
e.name = String(name || '').slice(0, 40); saveProfiles(list)
|
|
576
|
+
if (e.id === currentPid) { me = { ...(me || {}), nickname: e.name }; saveMe(me) }
|
|
577
|
+
return { id: e.id, name: e.name }
|
|
578
|
+
},
|
|
579
|
+
async deleteProfile ({ id } = {}) {
|
|
580
|
+
let list = loadProfiles()
|
|
581
|
+
if (list.length <= 1) throw new Error('no se puede borrar el único perfil')
|
|
582
|
+
if (!list.find((p) => p.id === id)) throw new Error('perfil no existe')
|
|
583
|
+
list = list.filter((p) => p.id !== id); saveProfiles(list)
|
|
584
|
+
// Borrado directo del namespace del perfil (incluye su store del vault si lo tuviera).
|
|
585
|
+
for (const s of ['keypair', 'enc-keypair', 'me', 'nonces', 'delegations', 'revocations', 'vault.device', 'vault.cert']) {
|
|
586
|
+
rawKv.removeItem(`dotrino.identity.p.${id}.${s}`)
|
|
587
|
+
}
|
|
588
|
+
if (currentPid === id) { currentPid = list[0].id; rawKv.setItem(CURRENT_STORAGE, currentPid) }
|
|
589
|
+
return { ok: true, current: currentPid }
|
|
590
|
+
},
|
|
591
|
+
|
|
514
592
|
// ----- emparejar ESTE dispositivo con el vault del usuario (Fase 1) -----
|
|
515
593
|
// Genera D aquí dentro (su privada NUNCA sale de la identidad), hace el enroll
|
|
516
594
|
// endurecido por el proxy y guarda el cert. NO cambia signData todavía (Fase 2).
|
|
@@ -545,7 +623,8 @@ export async function createIdentityCore ({ kv, peers, makeSync = null }) {
|
|
|
545
623
|
async vaultSign ({ payload }) {
|
|
546
624
|
const v = loadVaultCert(); const device = loadVaultDevice()
|
|
547
625
|
if (!v?.cert || !device) throw new Error('este dispositivo no está emparejado con un vault')
|
|
548
|
-
return remoteSign({ master: v.master, proxy: v.proxy, device, cert: v.cert, payload })
|
|
626
|
+
try { return await remoteSign({ master: v.master, proxy: v.proxy, device, cert: v.cert, payload }) }
|
|
627
|
+
catch (e) { return handleVaultError(e) }
|
|
549
628
|
},
|
|
550
629
|
|
|
551
630
|
// Store DELEGADO: lee/escribe el store de hilos+aperturas EN tu vault (con el cert).
|
|
@@ -553,14 +632,16 @@ export async function createIdentityCore ({ kv, peers, makeSync = null }) {
|
|
|
553
632
|
async vaultStore ({ method, args }) {
|
|
554
633
|
const v = loadVaultCert(); const device = loadVaultDevice()
|
|
555
634
|
if (!v?.cert || !device) throw new Error('este dispositivo no está emparejado con un vault')
|
|
556
|
-
return remoteStore({ master: v.master, proxy: v.proxy, device, cert: v.cert, method, args })
|
|
635
|
+
try { return await remoteStore({ master: v.master, proxy: v.proxy, device, cert: v.cert, method, args }) }
|
|
636
|
+
catch (e) { return handleVaultError(e) }
|
|
557
637
|
},
|
|
558
638
|
|
|
559
639
|
// Lista (solo lectura) de dispositivos enrolados en tu vault.
|
|
560
640
|
async listVaultDevices () {
|
|
561
641
|
const v = loadVaultCert(); const device = loadVaultDevice()
|
|
562
642
|
if (!v?.cert || !device) throw new Error('este dispositivo no está emparejado con un vault')
|
|
563
|
-
return remoteDevices({ master: v.master, proxy: v.proxy, device, cert: v.cert })
|
|
643
|
+
try { return await remoteDevices({ master: v.master, proxy: v.proxy, device, cert: v.cert }) }
|
|
644
|
+
catch (e) { return handleVaultError(e) }
|
|
564
645
|
},
|
|
565
646
|
|
|
566
647
|
// El cert de delegación de este dispositivo (para presentarlo al proxy en `identify`
|
|
@@ -671,6 +752,24 @@ export async function createIdentityCore ({ kv, peers, makeSync = null }) {
|
|
|
671
752
|
|
|
672
753
|
// ----- bootstrap -----
|
|
673
754
|
|
|
755
|
+
// Perfil activo (multi-perfil por dispositivo). Sin migración (ecosistema nuevo, no importa
|
|
756
|
+
// perder data): si no hay perfiles, se crea uno fresco. A partir de acá `kv` está scopeado a
|
|
757
|
+
// `currentPid` y `peers` apunta al peer book de ese perfil.
|
|
758
|
+
{
|
|
759
|
+
let profiles = loadProfiles()
|
|
760
|
+
currentPid = rawKv.getItem(CURRENT_STORAGE)
|
|
761
|
+
if (!profiles.length) {
|
|
762
|
+
currentPid = 'p' + crypto.randomUUID().slice(0, 8)
|
|
763
|
+
profiles = [{ id: currentPid, name: '', pubkey: null }]
|
|
764
|
+
saveProfiles(profiles)
|
|
765
|
+
rawKv.setItem(CURRENT_STORAGE, currentPid)
|
|
766
|
+
} else if (!currentPid || !profiles.find((p) => p.id === currentPid)) {
|
|
767
|
+
currentPid = profiles[0].id
|
|
768
|
+
rawKv.setItem(CURRENT_STORAGE, currentPid)
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
await peers.setProfile?.(currentPid)
|
|
772
|
+
|
|
674
773
|
keypair = await loadOrCreateKeypair()
|
|
675
774
|
publickeyJwkStr = JSON.stringify(keypair.publicJwk)
|
|
676
775
|
encKeypair = await loadOrCreateEncKeypair()
|
|
@@ -690,6 +789,14 @@ export async function createIdentityCore ({ kv, peers, makeSync = null }) {
|
|
|
690
789
|
kv.setItem(ME_STORAGE, JSON.stringify(me))
|
|
691
790
|
}
|
|
692
791
|
|
|
792
|
+
// Registrar el pubkey (y nombre) del perfil activo en su meta → para avatar/listado sin abrir cada perfil.
|
|
793
|
+
{
|
|
794
|
+
const list = loadProfiles(); const e = list.find((p) => p.id === currentPid)
|
|
795
|
+
if (e && (e.pubkey !== publickeyJwkStr || (!e.name && me?.nickname))) {
|
|
796
|
+
e.pubkey = publickeyJwkStr; if (!e.name && me?.nickname) e.name = me.nickname; saveProfiles(list)
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
693
800
|
if (typeof makeSync === 'function') {
|
|
694
801
|
sync = makeSync({
|
|
695
802
|
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
|
}
|