@dotrino/identity 0.103.0 → 0.104.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/identity",
3
- "version": "0.103.0",
3
+ "version": "0.104.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",
@@ -0,0 +1,80 @@
1
+ /**
2
+ * TODO LO DE LA IDENTIDAD EN LA APP, NO EN LA PÁGINA (app de iOS, 2026-09-26).
3
+ *
4
+ * WebKit guarda el localStorage y el IndexedDB de un iframe de otro origen APARTE por cada
5
+ * página que lo contiene. Dentro de la app de iOS eso partía la identidad en tantas como
6
+ * páginas: `dotrino.com`, `profile.dotrino.com` y `vault.dotrino.com` veían cada una un
7
+ * perfil distinto, recién inventado (dueño: «todas deberían tener el perfil/perfiles
8
+ * cargados nativamente»). El WebView de Android no separa así, y por eso allí no pasaba.
9
+ *
10
+ * Con esto, cuando la app lo ofrece (`bridge.storage`), las tres piezas que la identidad
11
+ * guarda —el `kv` síncrono, los registros de llaves y el libro de contactos— viven en UN
12
+ * almacén de la app, el mismo para todas las páginas. Nada secreto viaja: las llaves son del
13
+ * chip (el registro es `{ external: kid, kind, publicJwk }`) y el resto es lo que ya se
14
+ * guardaba en el disco de la página.
15
+ *
16
+ * Cómo: al arrancar se trae TODO de una vez (`storeLoad`) y se sirve desde memoria, que es
17
+ * lo que el `kv` necesita para seguir siendo síncrono. Cada cambio se escribe en la app al
18
+ * momento, en orden. Solo hay un iframe de identidad vivo a la vez (la app tiene un solo
19
+ * WebView), así que no hay dos memorias que se pisen.
20
+ */
21
+
22
+ const KV = 'kv:'
23
+ const KEY = 'key:'
24
+ const PEERS = 'peers:'
25
+
26
+ /** El asa de una llave no se puede guardar en la app: solo su registro, que no tiene nada secreto. */
27
+ function plainKeyRecord (rec) {
28
+ if (rec && typeof rec.external === 'string' && rec.publicJwk && (rec.kind === 'sign' || rec.kind === 'enc')) {
29
+ return { external: rec.external, kind: rec.kind, publicJwk: rec.publicJwk }
30
+ }
31
+ // Una llave de SOFTWARE (adoptar un perfil, entrar con contraseña) no cabe en el chip, y
32
+ // guardarla a medias sería perder la cuenta sin decirlo. Se para aquí y se dice.
33
+ throw Object.assign(new Error('this app keeps keys in the phone chip only: a software key cannot be stored here'), { code: 'native-no-import' })
34
+ }
35
+
36
+ export async function nativeBackends (bridge) {
37
+ const { items } = await bridge.call('storeLoad', {})
38
+ if (!items || typeof items !== 'object') {
39
+ throw Object.assign(new Error('the app did not return its store'), { code: 'native-no-store' })
40
+ }
41
+ const mem = new Map(Object.entries(items))
42
+
43
+ // Escrituras EN ORDEN: un `remove` que adelantara a su `set` resucitaría el dato. Una que
44
+ // falla se dice en la consola con su código (sin tragársela) y no frena a las siguientes.
45
+ let chain = Promise.resolve()
46
+ const write = (method, params) => {
47
+ chain = chain.then(() => bridge.call(method, params)).catch((e) => {
48
+ console.error(`[cc-identity] native store ${method} failed (${e?.code || 'error'}): ${e?.message}`)
49
+ })
50
+ return chain
51
+ }
52
+ const set = (k, v) => { mem.set(k, v); return write('storeSet', { k, v }) }
53
+ const del = (k) => { mem.delete(k); return write('storeRemove', { k }) }
54
+
55
+ const kv = {
56
+ getItem: (k) => (mem.has(KV + k) ? mem.get(KV + k) : null),
57
+ setItem: (k, v) => { set(KV + k, String(v)) },
58
+ removeItem: (k) => { del(KV + k) }
59
+ }
60
+
61
+ const keyStore = {
62
+ async get (name) {
63
+ const raw = mem.get(KEY + name)
64
+ return raw ? JSON.parse(raw) : null
65
+ },
66
+ async set (name, rec) { await set(KEY + name, JSON.stringify(plainKeyRecord(rec))) },
67
+ async remove (name) { await del(KEY + name) }
68
+ }
69
+
70
+ // El libro de contactos por perfil (`peerStore.useBackend`).
71
+ const peers = {
72
+ async get (key) {
73
+ const raw = mem.get(PEERS + key)
74
+ return raw ? JSON.parse(raw) : null
75
+ },
76
+ async put (key, val) { await set(PEERS + key, JSON.stringify(val)) }
77
+ }
78
+
79
+ return { kv, keyStore, peers, flush: () => chain }
80
+ }
@@ -40,6 +40,14 @@ let _pid = null
40
40
  */
41
41
  let _volatile = false
42
42
 
43
+ /**
44
+ * Otro almacén para el libro, en vez del IndexedDB de la página: el de la app de iOS
45
+ * (`nativeStore.js`), que es uno para todas las páginas. `{ get(key), put(key, val) }`.
46
+ * Se pone antes de `initPeerStorage`.
47
+ */
48
+ let _backend = null
49
+ export function useBackend (b) { _backend = b || null }
50
+
43
51
  /** Registra el callback que marca el estado como "sucio" para el sync. */
44
52
  export function onDirty (fn) { _markDirty = fn }
45
53
 
@@ -53,6 +61,7 @@ function peersKey () { return _pid ? `peers.${_pid}.v1` : IDB_PEERS_KEY }
53
61
 
54
62
  /** Migración: copia el peer book VIEJO (pre-multi-perfil, sin namespace) al perfil `pid`. */
55
63
  export async function adoptLegacy (pid) {
64
+ if (_backend) return // el almacén de la app nació después: no hay libro viejo que adoptar
56
65
  try {
57
66
  _idb = _idb || await openIdb()
58
67
  let legacy = await idbGet(_idb, IDB_PEERS_KEY) // 'peers.v1' (viejo)
@@ -103,6 +112,11 @@ export async function initPeerStorage () {
103
112
  try { if (typeof navigator !== 'undefined' && navigator.storage?.persist) await navigator.storage.persist() }
104
113
  catch (_) { /* best-effort */ }
105
114
  if (_volatile) { _peers = {}; return _peers }
115
+ if (_backend) {
116
+ const stored = await _backend.get(peersKey())
117
+ _peers = (stored && typeof stored === 'object') ? stored : {}
118
+ return _peers
119
+ }
106
120
  try {
107
121
  _idb = await openIdb()
108
122
  const stored = await idbGet(_idb, peersKey()) // peer book DEL perfil activo (namespaceado)
@@ -120,6 +134,13 @@ export async function initPeerStorage () {
120
134
  function persistPeers () {
121
135
  if (_volatile) return _writeChain // cuenta volátil: en memoria y en ningún disco
122
136
  const key = peersKey()
137
+ if (_backend) {
138
+ const snapshot = _peers
139
+ _writeChain = _writeChain
140
+ .then(() => _backend.put(key, snapshot))
141
+ .catch(e => console.warn('[cc-identity] persist (app) failed:', e?.message))
142
+ return _writeChain
143
+ }
123
144
  if (_fallback || !_idb) {
124
145
  try { localStorage.setItem(key, JSON.stringify(_peers)) }
125
146
  catch (e) { console.warn('[cc-identity] persist (ls) failed:', e?.message) }
package/vault/vault.js CHANGED
@@ -15,10 +15,20 @@ import {
15
15
  import { createIdentityCore } from './core.js'
16
16
  import { pubkeyId } from './capabilities.js'
17
17
  import { withExternalKeys, appBridge } from './externalKeys.js'
18
+ import { nativeBackends } from './nativeStore.js'
19
+ import { useBackend as usePeerBackend } from './peerStore.js'
18
20
 
19
21
  ;(async () => {
22
+ // DENTRO DE LA APP, el puente con el teléfono. En un navegador no está y nada cambia.
23
+ const idKeys = typeof window !== 'undefined' ? window.DotrinoIdentityKeys : null
24
+ const bridge = idKeys ? appBridge(idKeys) : null
25
+ // Y si la app GUARDA la identidad (`storage`, la de iOS): todo vive allí, uno para todas
26
+ // las páginas, porque WebKit parte el almacén de este iframe por página (`./nativeStore.js`).
27
+ const native = bridge && idKeys.storage === true ? await nativeBackends(bridge) : null
28
+ if (native) usePeerBackend(native.peers)
29
+
20
30
  // kv estilo localStorage (síncrono) para me, nonces, delegaciones, certs.
21
- const kv = {
31
+ const kv = native ? native.kv : {
22
32
  getItem: (k) => localStorage.getItem(k),
23
33
  setItem: (k, v) => localStorage.setItem(k, v),
24
34
  removeItem: (k) => localStorage.removeItem(k)
@@ -28,7 +38,7 @@ import { withExternalKeys, appBridge } from './externalKeys.js'
28
38
  // IndexedDB (clonado estructurado). Nadie —ni este código, ni un XSS en este
29
39
  // origen— puede leer sus bytes; solo firmar/derivar con ellas. Las llaves
30
40
  // planas (JWK) viejas de localStorage se migran y se borran (core.js).
31
- const baseKeyStore = await (() => new Promise((resolve) => {
41
+ const baseKeyStore = native ? native.keyStore : await (() => new Promise((resolve) => {
32
42
  const req = indexedDB.open('dotrino-identity-keys', 1)
33
43
  req.onupgradeneeded = () => req.result.createObjectStore('keys')
34
44
  req.onsuccess = () => {
@@ -47,11 +57,10 @@ import { withExternalKeys, appBridge } from './externalKeys.js'
47
57
  }
48
58
  req.onerror = () => resolve(null) // sin IDB (raro): cae al modo kv legado
49
59
  }))()
50
- // DENTRO DE LA APP DE ANDROID, las llaves nuevas nacen en el chip del teléfono: una sola
60
+ // DENTRO DE LA APP (Android e iOS), las llaves nuevas nacen en el chip del teléfono: una sola
51
61
  // llave por cuenta, la misma que aprueba en la pantalla nativa (`./externalKeys.js`). El
52
62
  // puente solo existe en este origen; en un navegador no está y nada cambia.
53
- const idKeys = typeof window !== 'undefined' ? window.DotrinoIdentityKeys : null
54
- const keyStore = idKeys && baseKeyStore ? withExternalKeys(baseKeyStore, appBridge(idKeys)) : baseKeyStore
63
+ const keyStore = bridge && baseKeyStore ? withExternalKeys(baseKeyStore, bridge) : baseKeyStore
55
64
 
56
65
  // sessionKv: la prueba de desbloqueo del candado por contraseña vive en
57
66
  // sessionStorage — POR PESTAÑA: sobrevive al refresco, muere al cerrarla.