{"_id":"@closerclick/closer-click-store","_rev":"2-d01d923966358bb42bb591f651c4e212","name":"@closerclick/closer-click-store","dist-tags":{"latest":"0.4.0"},"versions":{"0.3.0":{"name":"@closerclick/closer-click-store","version":"0.3.0","keywords":["closer-click","store","messaging","cross-app"],"author":{"name":"seyacat"},"license":"MIT","_id":"@closerclick/closer-click-store@0.3.0","maintainers":[{"name":"mycloserclick","email":"admin@closer.click"}],"homepage":"https://github.com/seyacat/closer-click-store#readme","bugs":{"url":"https://github.com/seyacat/closer-click-store/issues"},"dist":{"shasum":"527786af1e2ee646adbd5225c69a209a175236d5","tarball":"https://registry.npmjs.org/@closerclick/closer-click-store/-/closer-click-store-0.3.0.tgz","fileCount":9,"integrity":"sha512-Iq5iKuZ0g92wwhDpEH6NEjJxuZUR+S/nsUHfI+NONPfdKWpyjXhqn4HGwgCFJHOQ0o6dKtbUqFsOL6tI6lpNSg==","signatures":[{"sig":"MEYCIQCBUoRa9EuCP7t8uy6DmVpTEzvtqnVerUSoRk7NoW52sQIhAJ9ikQ8JUBownbTaB1Oau6l4Af4y9dUI7TOwMc9Y3RUB","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":46838},"main":"src/index.js","type":"module","types":"src/index.d.ts","module":"src/index.js","exports":{".":{"types":"./src/index.d.ts","import":"./src/index.js"}},"gitHead":"bb7366fed9e83c58d0df659c3a8ed48aa3d730c7","scripts":{"test":"playwright test"},"_npmUser":{"name":"mycloserclick","email":"admin@closer.click"},"repository":{"url":"git+https://github.com/seyacat/closer-click-store.git","type":"git"},"_npmVersion":"11.12.1","description":"Almacén compartido de hilos de mensajes para el ecosistema Closer Click (vault iframe + postMessage)","directories":{},"_nodeVersion":"25.9.0","_hasShrinkwrap":false,"devDependencies":{"@playwright/test":"^1.49.0"},"_npmOperationalInternal":{"tmp":"tmp/closer-click-store_0.3.0_1779987190715_0.2795891500207648","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@closerclick/closer-click-store","version":"0.4.0","description":"Almacén compartido de hilos de mensajes para el ecosistema Closer Click (vault iframe + postMessage)","type":"module","main":"src/index.js","module":"src/index.js","types":"src/index.d.ts","exports":{".":{"types":"./src/index.d.ts","import":"./src/index.js"}},"scripts":{"test":"playwright test"},"devDependencies":{"@playwright/test":"^1.49.0"},"keywords":["closer-click","store","messaging","cross-app"],"author":{"name":"seyacat"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/closerclick/closer-click-store.git"},"gitHead":"41f9e16db9e7556727d98f03ef60a0f33835ce96","_id":"@closerclick/closer-click-store@0.4.0","bugs":{"url":"https://github.com/closerclick/closer-click-store/issues"},"homepage":"https://github.com/closerclick/closer-click-store#readme","_nodeVersion":"25.9.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-8jWlV+WIDtWecoh19tIXGXv8k9vkhlV9OtvijR5QXqkacVcYGUmeFdxg4spbm+/vo9rPynDhXoASXmkMmR0u/w==","shasum":"042244c16ff837f348863e1021636e91dbfd663f","tarball":"https://registry.npmjs.org/@closerclick/closer-click-store/-/closer-click-store-0.4.0.tgz","fileCount":9,"unpackedSize":49558,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCXGRm5ieVBulu+as9rinphk+C390toq5p/3+MuDIABswIhAN3X9VC3/DpqTsC7QeqQH9wDMflTOV0Zb82fMyP3K544"}]},"_npmUser":{"name":"mycloserclick","email":"admin@closer.click"},"directories":{},"maintainers":[{"name":"mycloserclick","email":"admin@closer.click"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/closer-click-store_0.4.0_1781108943533_0.5142504118777103"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-28T16:53:10.569Z","modified":"2026-06-10T16:29:03.888Z","0.3.0":"2026-05-28T16:53:10.904Z","0.4.0":"2026-06-10T16:29:03.739Z"},"bugs":{"url":"https://github.com/closerclick/closer-click-store/issues"},"author":{"name":"seyacat"},"license":"MIT","homepage":"https://github.com/closerclick/closer-click-store#readme","keywords":["closer-click","store","messaging","cross-app"],"repository":{"type":"git","url":"git+https://github.com/closerclick/closer-click-store.git"},"description":"Almacén compartido de hilos de mensajes para el ecosistema Closer Click (vault iframe + postMessage)","maintainers":[{"name":"mycloserclick","email":"admin@closer.click"}],"readme":"# @closerclick/closer-click-store\n\nAlmacén compartido de hilos de mensajes para el ecosistema [Closer Click](https://github.com/closerclick).\n\nMismo patrón que [closer-click-identity](https://github.com/closerclick/closer-click-identity): un iframe oculto servido desde `store.closer.click` mantiene los datos en su propio almacenamiento. Cualquier app del ecosistema (web messenger, extensión Chrome, futura app móvil PWA) que cargue este iframe en el mismo navegador comparte los mismos hilos.\n\n## Almacenamiento: IndexedDB (desde v0.3.0)\n\nEl backend del vault es **IndexedDB** (antes `localStorage`):\n\n- **Cuota grande y dinámica** según el disco (cientos de MB a GB), en vez del techo de ~5 MB de `localStorage`. Desaparece el riesgo de evicción del más viejo al llenarse el bucket compartido del origen.\n- Pide **`navigator.storage.persist()`** → almacenamiento **persistente / no-evictable** (best-effort).\n- **Migración automática** una sola vez: si hay datos en el `localStorage` anterior (`cc.store.threads.v1`), se copian a IndexedDB en el primer arranque.\n- Si IndexedDB no está disponible (p. ej. modo privado), cae a `localStorage` para no perder funcionalidad.\n- 100% local: no requiere cuenta ni terceros. (El **sync** opcional a tu Google Drive sigue aparte, cifrado y off por defecto.)\n- `getStats()` reporta `backend`, `usage`, `quota` y `persisted`.\n\n## Por qué un subdominio aparte\n\n- Los mensajes son volumen mucho mayor que las claves/contactos. Mantenerlos fuera del vault de identidad evita saturar ese almacenamiento.\n- Cada origen tiene su propia cuota. Subdominios distintos = aislar y sumar cuotas.\n- Permite evolucionar el schema de mensajería sin tocar el de identidad (más estable).\n\n## API\n\n```js\nimport { Store } from '@closerclick/closer-click-store'\n\nconst store = await Store.connect()  // singleton — carga el iframe oculto\n\n// El threadKey lo decide la app (típicamente la pubkey del contacto)\nawait store.appendMessage(contactPubkey, {\n  dir: 'out',\n  text: 'hola',\n  ts: Date.now()\n  // id se autogenera si no lo pasas\n})\n\nconst entries = await store.listThread(contactPubkey, { limit: 50 })\n\nconst summaries = await store.getThreadSummaries()\n// → { [pubkey]: { lastEntry, count } }   para sidebar de conversaciones\n\nawait store.removeThread(contactPubkey)\nawait store.clearAll()                    // borrar todo el almacén\n\nconst stats = await store.getStats()\n// → { totalBytes, threadCount, threads: { [k]: { count, bytes } },\n//     backend: 'indexeddb', usage, quota, persisted }\n```\n\n## Garantías\n\n- **Per-thread cap**: 1000 mensajes por defecto, configurable con `setMaxPerThread(n)`. El más antiguo se descarta al añadir uno nuevo si pasa el cap.\n- **Eviction global ante `QuotaExceededError`**: solo como red de seguridad (con IndexedDB es prácticamente inalcanzable). Descarta el 20% más antiguo a través de todos los hilos y reintenta hasta 8 veces.\n- **No sale del navegador**: nunca se hace fetch, no hay servidor, no hay analytics (salvo el sync opcional a tu Drive, off por defecto).\n\n## Deploy\n\nGitHub Actions despliega a `store.closer.click` cuando cambia algo en `store/`. El bundle del iframe es estático (HTML + JS, sin build).\n\n## Schema\n\nIndexedDB `cc-store` → object store `kv` → key `threads.v1`:\n\n```\nvalue: JSON { [threadKey: string]: ThreadEntry[] }\n```\n\nLas entradas son objetos opacos para el store; solo se les pide `id` y `ts` para deduplicación y ordenamiento.\n\n## Tests\n\n```bash\nnpm install\nnpm test        # Playwright: sirve el vault y ejercita los handlers vía postMessage\n                # (IndexedDB real: append/list/dedup, persistencia, migración, stats…)\n```\n\n## Auto-sync con Google Drive (0.2.0+)\n\nBackup cifrado y sync multi-dispositivo de los hilos contra `appDataFolder` de Google Drive. Mismo modelo y API que [`@closerclick/closer-click-identity`](https://github.com/closerclick/closer-click-identity#auto-sync-con-google-drive-080) — los mensajes se cifran con AES-256-GCM (clave derivada por PBKDF2 600 000 iter de la passphrase) antes de subirse, así que Google solo ve bytes opacos.\n\n```js\nawait store.syncConnect(clientId)              // OAuth popup (scope: drive.appdata)\nawait store.syncUnlock('mi-passphrase')        // ≥12 chars\nstore.onSync(ev => console.log(ev.status))    // syncing | synced | offline | conflict | error\nawait store.syncNow()                          // forzar pull+push\n```\n\n**Merge de hilos**: unión por `id`, dedup, last-writer por `ts`, ordena ascendente, aplica `maxPerThread` después del merge. Append-only así que el merge es trivial — si dos dispositivos añaden mensajes a la vez, el resultado contiene los dos sets sin pérdida.\n\nNuevos métodos también para export/import manual:\n\n```js\nconst { threads } = await store.exportThreads()\nawait store.importThreads(threads, 'merge')   // o 'replace'\n```\n","readmeFilename":"README.md"}