{"_id":"@closerclick/closer-click-identity","_rev":"3-f146db9dccce85de6b8a342759628afd","name":"@closerclick/closer-click-identity","dist-tags":{"latest":"0.10.0"},"versions":{"0.8.0":{"name":"@closerclick/closer-click-identity","version":"0.8.0","keywords":["identity","rating","closer-click","vault","cross-origin"],"author":{"name":"seyacat"},"license":"MIT","_id":"@closerclick/closer-click-identity@0.8.0","maintainers":[{"name":"mycloserclick","email":"admin@closer.click"}],"homepage":"https://github.com/seyacat/closer-click-identity#readme","bugs":{"url":"https://github.com/seyacat/closer-click-identity/issues"},"dist":{"shasum":"b8c85e6276bf65677dd36835828c357a45848519","tarball":"https://registry.npmjs.org/@closerclick/closer-click-identity/-/closer-click-identity-0.8.0.tgz","fileCount":9,"integrity":"sha512-aAYrDBpz6fq4WmTidZ6H72vAh7EHrtbLpvNqaWHiIogFojrusF8dFBWNZLI2GdZXg8pyvnsLRDleyLz+gDY+Tg==","signatures":[{"sig":"MEUCIBNwV6Emd3L3Suq7aP4uMDa4PLduexRO/8GDghNTInpVAiEAproWDkOTYeJ/46XBdQOZz3WffjFuWFnCOCkCYraXpHw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":73404},"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":"a9ca42b53de2c868008c1d5762cfb59aa58af9a2","scripts":{"test":"node --test test/"},"_npmUser":{"name":"mycloserclick","email":"admin@closer.click"},"repository":{"url":"git+https://github.com/seyacat/closer-click-identity.git","type":"git"},"_npmVersion":"11.12.1","description":"Identidad y rating de usuarios compartidos entre apps de Closer Click (vault iframe + postMessage)","directories":{},"_nodeVersion":"25.9.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/closer-click-identity_0.8.0_1779987179701_0.4415589314533075","host":"s3://npm-registry-packages-npm-production"}},"0.9.1":{"name":"@closerclick/closer-click-identity","version":"0.9.1","keywords":["identity","rating","closer-click","vault","cross-origin"],"author":{"name":"seyacat"},"license":"MIT","_id":"@closerclick/closer-click-identity@0.9.1","maintainers":[{"name":"mycloserclick","email":"admin@closer.click"}],"homepage":"https://github.com/closerclick/closer-click-identity#readme","bugs":{"url":"https://github.com/closerclick/closer-click-identity/issues"},"dist":{"shasum":"8315f8098906226ed63fd21e1a24ec6751ebe7c9","tarball":"https://registry.npmjs.org/@closerclick/closer-click-identity/-/closer-click-identity-0.9.1.tgz","fileCount":12,"integrity":"sha512-aa3Ih7jKSDOrUnHLk1N1LiG1H8969CmNKgSlbZS2gdb2iyFvSfmc2oP5y1+wW3ZLaXxTUlYQiD8M1OqWEAkZnA==","signatures":[{"sig":"MEUCIQD7YxoJ9rHBug0qpHABYQCQWJgQTks4jgqKCQxnbQrEmQIgWpAGrU7//E3nyva5nDdRzVeGc1LJFDrJEKsc/vuYubg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":84633},"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"},"./node":{"import":"./src/node.js"},"./vault/core.js":"./vault/core.js"},"gitHead":"db056034c6bb5b549f434882d2ce75b7b2640cfa","scripts":{"test":"node --test test/"},"_npmUser":{"name":"mycloserclick","email":"admin@closer.click"},"repository":{"url":"git+https://github.com/closerclick/closer-click-identity.git","type":"git"},"_npmVersion":"11.12.1","description":"Identidad y rating de usuarios compartidos entre apps de Closer Click (vault iframe + postMessage)","directories":{},"_nodeVersion":"25.9.0","_hasShrinkwrap":false,"devDependencies":{"fake-indexeddb":"^6.2.5"},"_npmOperationalInternal":{"tmp":"tmp/closer-click-identity_0.9.1_1780587831562_0.06032400127055304","host":"s3://npm-registry-packages-npm-production"}},"0.10.0":{"name":"@closerclick/closer-click-identity","version":"0.10.0","description":"Identidad y rating de usuarios compartidos entre apps de 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"},"./node":{"import":"./src/node.js"},"./capabilities":{"import":"./vault/capabilities.js"},"./vault/core.js":"./vault/core.js"},"scripts":{"test":"node --test test/*.test.js"},"keywords":["identity","rating","closer-click","vault","cross-origin"],"author":{"name":"seyacat"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/closerclick/closer-click-identity.git"},"devDependencies":{"fake-indexeddb":"^6.2.5"},"gitHead":"4f2f0d5f904e51e9006f3c6d4cb028001bd0b124","_id":"@closerclick/closer-click-identity@0.10.0","bugs":{"url":"https://github.com/closerclick/closer-click-identity/issues"},"homepage":"https://github.com/closerclick/closer-click-identity#readme","_nodeVersion":"25.9.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-s6/JHbUl5OtZXNzk/Fy5dY5TYpOzavJRPaDE5EmkyO7cpTXIFrq4IX2v3AdcSgfIXN+OOSCCB24zHnMC4EEWVA==","shasum":"07e5d9d57a21778dc00bcdc3825458b464fb15a4","tarball":"https://registry.npmjs.org/@closerclick/closer-click-identity/-/closer-click-identity-0.10.0.tgz","fileCount":13,"unpackedSize":100246,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCLRqDOpH2wP6GOSxGnUXARANfpEeIahe8MCo2JD/P6/QIhAMIO0F7FhCd2c9oLM2iWTpgpmaQMQxvS+cNzsHBlX3Lo"}]},"_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-identity_0.10.0_1780888046644_0.2555925845379148"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-28T16:52:59.587Z","modified":"2026-06-08T03:07:26.937Z","0.8.0":"2026-05-28T16:52:59.865Z","0.9.1":"2026-06-04T15:43:51.726Z","0.10.0":"2026-06-08T03:07:26.807Z"},"bugs":{"url":"https://github.com/closerclick/closer-click-identity/issues"},"author":{"name":"seyacat"},"license":"MIT","homepage":"https://github.com/closerclick/closer-click-identity#readme","keywords":["identity","rating","closer-click","vault","cross-origin"],"repository":{"type":"git","url":"git+https://github.com/closerclick/closer-click-identity.git"},"description":"Identidad y rating de usuarios compartidos entre apps de Closer Click (vault iframe + postMessage)","maintainers":[{"name":"mycloserclick","email":"admin@closer.click"}],"readme":"# @closerclick/closer-click-identity\n\nIdentidad de usuario y rating de peers compartidos entre las apps de Closer Click. Funciona aunque las apps vivan en orígenes distintos: usa un **vault iframe** alojado en un origin estable que guarda la información en su propio `localStorage` y expone una API por `postMessage`.\n\n## Cómo funciona\n\n```\n┌────────────────────┐    postMessage    ┌────────────────────────┐\n│  app (cualquier    │ ◀───────────────▶ │  vault iframe          │\n│  origin: chat,     │                   │  origin: id.closer     │\n│  qrshare, chess…)  │                   │  .click                │\n│                    │                   │  - keypair ECDSA P-256 │\n│  import {Identity} │                   │  - keypair ECDH P-256  │\n└────────────────────┘                   │  - peers + ratings     │\n                                         └────────────────────────┘\n```\n\nComo todas las apps cargan el vault desde el mismo origin, comparten el mismo `localStorage` aunque ellas estén en orígenes distintos. **Las claves privadas nunca salen del vault** — las apps reciben firmas (de la ECDSA) y plaintext descifrado (de la ECDH) pero nunca las llaves.\n\n## Instalación\n\n```bash\nnpm install @closerclick/closer-click-identity\n```\n\n## Uso\n\n```js\nimport { Identity } from '@closerclick/closer-click-identity'\n\nconst id = await Identity.connect({\n  vaultUrl: 'https://id.closer.click/'   // por defecto\n})\n\nconsole.log('my publickey JWK:', id.me.publickey)\n\n// Identificar a un peer (handshake challenge/response)\nconst { nonce } = await id.makeChallenge()\n// envía nonce al peer por el canal que sea (proxy, postMessage, etc.)\n// el peer ejecuta await id.signChallenge(nonce) y te devuelve { nonce, publickey, signature }\nconst verification = await id.verifyResponse(response)\nif (verification.ok) {\n  console.log('peer verificado:', verification.publickey)\n  await id.setNickname(verification.publickey, 'Bob de chess')\n  await id.setRating(verification.publickey, 4, 'buen rival')\n}\n\nconst peers = await id.listPeers()\n```\n\n## Hosting del vault\n\nPara que distintas apps compartan datos, todas deben apuntar al **mismo `vaultUrl`**. La carpeta `vault/` de este paquete es un sitio estático (HTML + JS) listo para subir a:\n\n- Un dominio dedicado: `id.closer.click`\n- O temporalmente: `https://id.closer.click/vault/`\n\nImportante: subir vía HTTPS y configurar `Content-Security-Policy: frame-ancestors *` (o lista de orígenes permitidos) para permitir que las apps lo embeban.\n\n## API\n\n### `Identity.connect(options?)`\nInicializa el iframe y resuelve cuando el vault está listo.\n\n| opción       | tipo     | default                       |\n|--------------|----------|-------------------------------|\n| `vaultUrl`   | string   | `https://id.closer.click/` |\n| `timeoutMs`  | number   | `5000`                        |\n\n### Identidad propia\n\n- `id.me`                            → `{ publickey, nickname? }`\n- `id.setMyNickname(nickname)`\n\n### Handshake\n\n- `id.makeChallenge()`               → `{ nonce }`\n- `id.signChallenge(nonce)`          → `{ nonce, publickey, encryptionPubkey, signature }`\n- `id.verifyResponse(response)`      → `{ ok, publickey?, encryptionPubkey?, peer? }`\n\n### Peer book\n\n- `id.getPeer(publickey)`\n- `id.setNickname(publickey, nickname)`\n- `id.setRating(publickey, rating, notes?)` — produce un envelope firmado y lo guarda como `peer.myRating` (rating 0–5).\n- `id.mergeEndorsements(subject, [signedRatings], askerPubkey?)` — para web-of-trust: valida firmas, dedupea por `(ratedBy, subject)`, cap 50.\n- `id.getRatingsForSubject(subject)` → `{ mine, endorsements }` para responder a un `RATING_QUERY`.\n- `id.recordQuery(askerPubkey, subject?)` — contabiliza consultas para el suspicion modifier.\n- `id.listPeers()`, `id.forgetPeer(publickey)`\n\n### Contactos compartidos (0.6.0+)\n\nMismo registro de `peers` que arriba, pero filtrado por flag `isContact: true`. Cualquier app del ecosistema (chat, chess, messenger, extensión) puede añadir/leer contactos del mismo address book.\n\n- `id.addContact({ publickey, nickname?, encryptionPubkey?, lastToken?, notes? })`\n- `id.updateContact(publickey, patch)` — patch limitado a `nickname`, `encryptionPubkey`, `lastToken`, `contactNotes`.\n- `id.removeContact(publickey)` — quita el flag `isContact` pero conserva ratings/endorsements.\n- `id.listContacts()` → array filtrado, ordenado por `lastSeen` desc.\n\n### Encripción E2E (0.5.0+)\n\n- `id.getEncryptionPubkey()` → JWK string del propio peer.\n- `id.encrypt(recipients, plaintext)` → `{v:1, iv, ct, wrap}` envelope. `recipients` = `[{token, encryptionPubkey}]`. AES-256-GCM con clave efímera por mensaje, envuelta para cada destinatario vía ECDH(P-256). El campo `token` puede ser cualquier identificador estable (en messenger se usa la pubkey del destinatario para sobrevivir cambios de token del proxy).\n- `id.decrypt(senderEncryptionPubkey, myToken, envelope)` → `{ plaintext }`. Forward-secrecy por mensaje (clave simétrica nueva cada vez).\n\n### Firma genérica (0.7.0+)\n\n- `id.signData(data)` → `{ signature, publickey }` con encoding canonical-JSON. Lo usa el messenger para construir sobres `identify` que el proxy verifica con su `verifySignatureWithJWK`.\n\n### Backup / migración\n\n- `id.exportIdentity()` → blob JSON con `privateJwk` (ECDSA), `encPrivateJwk` (ECDH), `me`, `peers`. **Sensible** — el host app es responsable de guardarlo de manera segura.\n- `id.importIdentity(blob)` → reemplaza la identidad local. Soporta blobs v1 (sin ECDH) y v2 (con ambas keys).\n\n### Auto-sync con Google Drive (0.8.0+)\n\nBackup automático y sincronización multi-dispositivo de la identidad (claves + contactos + ratings + endorsements) usando Google Drive como almacén opaco. **Google nunca ve tus datos en claro**: el blob se cifra en el navegador con AES-256-GCM y una clave derivada por PBKDF2 (600 000 iteraciones) de una passphrase elegida por el usuario.\n\nTopología:\n- El blob se guarda en la carpeta especial `appDataFolder` de Drive (oculta para el usuario, no contamina su Drive).\n- Scope OAuth requerido: solo `https://www.googleapis.com/auth/drive.appdata`.\n- El cliente OAuth (Google Cloud Console → Web application) debe tener `https://id.closer.click` (y/o `http://localhost:5173` para dev) como Authorized JavaScript Origin.\n- El sync corre **dentro del iframe del vault**: las claves privadas nunca cruzan el postMessage boundary.\n\n```js\n// 1. Conectar Google (popup OAuth, una vez por origen)\nawait id.syncConnect('123456789-abc...apps.googleusercontent.com')\n\n// 2. Desbloquear con passphrase (≥12 chars). Se guarda en sessionStorage del vault\n//    (per-tab) y se borra al cerrar la pestaña.\nawait id.syncUnlock('mi-passphrase-larga-y-secreta')\n\n// 3. A partir de aquí: pull-on-unlock + push debounced (5s) + pull periódico (2 min).\n//    Cualquier mutación local (setRating, addContact, mergeEndorsements...) marca\n//    dirty automáticamente.\n\n// Eventos de estado\nid.onSync(({ status, error }) => console.log('sync:', status, error || ''))\n// status: connected | unlocked | syncing | synced | conflict | offline | error | locked | disconnected\n\n// Forzar pull+push inmediato\nawait id.syncNow()\n\n// Bloquear (limpia la passphrase de memoria)\nawait id.syncLock()\n```\n\n**Estrategia de merge** (al hacer pull, si remoto tiene cambios):\n- Keypairs ECDSA/ECDH: **nunca se sobreescriben**. Solo se adopta el remoto si local está vacío (primer setup en device nuevo).\n- Contact metadata (nickname, notes, encryptionPubkey, rating): last-writer-wins por `lastSeen`.\n- `myRating`: el envelope firmado con mayor `issuedAt` gana.\n- `endorsements`: unión dedup por `ratedBy`; firmas re-verificadas antes de aceptar.\n- Concurrencia: optimistic-lock con `If-Match: <etag>` en Drive. Si 412, pull → merge → push (3 reintentos).\n\n**Trade-off**: si el usuario pierde la passphrase, el blob es irrecuperable. Es coherente con E2E — ni Google ni el desarrollador pueden recuperarlo.\n\n## Diseño\n\n- **Una sola identidad por navegador**, persistente entre apps que apuntan al mismo vault.\n- **Sin servidor**: todo en `localStorage` del vault.\n- **Replay protection**: cada `makeChallenge` registra el nonce; `verifyResponse` exige que el nonce sea reciente (≤ 5 min).\n- **Privacidad**: la clave privada vive solo en el vault, las apps nunca la ven.\n\n## Licencia\n\nMIT\n","readmeFilename":"README.md"}