@dotrino/identity 0.48.0 → 0.49.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.48.0",
3
+ "version": "0.49.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",
package/src/node.js CHANGED
@@ -122,7 +122,11 @@ export class Identity {
122
122
  this._core = await createIdentityCore({
123
123
  kv: fileKv(path.join(this._dir, 'identity.json'), this._atRest),
124
124
  peers: filePeers(path.join(this._dir, 'peers.json')),
125
- makeSync: null
125
+ makeSync: null,
126
+ // Aquí las cuentas las lleva quien hospeda (el daemon del vault tiene su propio
127
+ // registro de perfiles): que una expulsión borre una por debajo le dejaría el suyo
128
+ // apuntando a algo que ya no existe. Se borra el enlace y el acta, nada más.
129
+ removeAccountOnExpulsion: false
126
130
  })
127
131
  this._core.onSyncStatus((payload) => this._emit('sync', payload))
128
132
  this._core.onVaultEvent((payload) => this._emit('vault', payload))
package/vault/core.js CHANGED
@@ -158,7 +158,7 @@ function sanitizeProfilePatch (patch = {}) {
158
158
  return out
159
159
  }
160
160
 
161
- export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, keyStore = null, sessionKv = null }) {
161
+ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, keyStore = null, sessionKv = null, removeAccountOnExpulsion = true }) {
162
162
  const {
163
163
  initPeerStorage, loadPeers, savePeers, setPeersDirect, upsertPeer, onDirty
164
164
  } = peers
@@ -350,18 +350,25 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
350
350
  * de perfiles, con su nombre y su foto, sin poder hacer nada y sin que nadie supiera qué
351
351
  * era ni cómo quitarlo.
352
352
  *
353
- * El aparato NUNCA se queda sin cuenta utilizable, que es la otra mitad: si había otras,
354
- * se pasa a una de ellas; si esa era la única, se estrena una vacía. Así al terminar hay
355
- * exactamente lo que tiene que haber: un dispositivo con su cuenta, listo para usarse o
356
- * para volver a conectarse a una bóveda.
353
+ * El aparato no se queda sin cuenta utilizable: al recargar, el arranque estrena una si
354
+ * no quedó ninguna, o entra en la primera que haya. Así al terminar hay exactamente lo
355
+ * que tiene que haber: un dispositivo con su cuenta, listo para usarse o para volver a
356
+ * conectarse a una bóveda.
357
357
  *
358
358
  * Los pasos van EN ESTE ORDEN a propósito:
359
359
  * 1. fuera el enlace y el acta (deja de poder hablar con la bóveda y de enseñar el
360
360
  * perfil del que lo echaron);
361
361
  * 2. 'revoked' → `@dotrino/store` borra el store de ESE perfil (apunta al id que ya
362
362
  * tenía fijado, así que da igual lo que hagamos después con el perfil activo);
363
- * 3. se borra la cuenta y se deja otra puesta;
364
- * 4. 'account-removed' → la app RECARGA (multi-perfil no es reactivo, por diseño).
363
+ * 3. se borra la cuenta;
364
+ * 4. 'account-removed' → la app RECARGA (multi-perfil no es reactivo, por diseño), y
365
+ * es el ARRANQUE quien deja puesta la que toque: si no queda ninguna estrena la
366
+ * primera, y si quedan cae a la primera de la lista. Esa decisión ya vivía ahí.
367
+ *
368
+ * El paso 3 es SOLO del navegador (`removeAccountOnExpulsion`). En Node las cuentas las
369
+ * lleva quien hospeda —el daemon del vault tiene su propio registro de perfiles, en
370
+ * `profiles.js`— y borrar una por debajo le dejaría el suyo apuntando a algo que ya no
371
+ * existe. Ahí se borra el enlace y el acta, y de la cuenta decide su dueño.
365
372
  */
366
373
  const wipeVaultLink = () => {
367
374
  try { kv.removeItem(VAULT_CERT_STORAGE); kv.removeItem(VAULT_DEVICE_STORAGE) } catch (_) {}
@@ -369,33 +376,54 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
369
376
  emitVault({ phase: 'revoked' })
370
377
  // Sin `await`: quien llama es un manejador de mensajes que no espera nada (y si algo
371
378
  // aquí revienta, el enlace y el acta ya se fueron, que es lo que no puede quedar).
372
- removeThisAccount().catch(() => {})
379
+ if (removeAccountOnExpulsion) removeThisAccount().catch(() => {})
373
380
  }
374
381
 
375
- /**
376
- * Borra la cuenta de ESTE dispositivo y deja otra activa (existente o recién creada).
377
- * Se usa al ser expulsado; ver `wipeVaultLink`.
378
- */
382
+ /** Borra la cuenta de ESTE dispositivo al ser expulsado; ver `wipeVaultLink`. */
379
383
  let removingAccount = false
380
384
  async function removeThisAccount () {
381
- // UNA VEZ. Puede haber varias peticiones en vuelo y a todas les llega el mismo aviso;
382
- // sin este cerrojo, la segunda borraría la cuenta RECIÉN CREADA (para entonces
383
- // `currentPid` ya es la nueva) en vez de la que echaron.
385
+ // UNA VEZ: puede haber varias peticiones en vuelo y a todas les llega el mismo aviso.
384
386
  if (removingAccount) return
385
387
  removingAccount = true
386
388
  const gone = currentPid
387
- // Si esa era la única cuenta hay que estrenar otra ANTES de borrarla: `deleteProfile`
388
- // se niega a dejar el dispositivo sin ninguna, y con razón. Crear ya deja la nueva
389
- // activa; si había otras, es el propio `deleteProfile` quien pasa a la primera.
390
- const created = loadProfiles().filter((p) => p.id !== gone).length === 0
391
- try {
392
- if (created) await handlers.createProfile({ name: '' })
393
- await handlers.deleteProfile({ id: gone })
394
- } catch (e) {
395
- emitVault({ phase: 'account-removed', removed: gone, current: currentPid, error: e?.message || String(e) })
389
+ try { await purgeProfile(gone) } catch (e) {
390
+ emitVault({ phase: 'account-removed', removed: gone, error: e?.message || String(e) })
396
391
  return
397
392
  }
398
- emitVault({ phase: 'account-removed', removed: gone, current: currentPid, created })
393
+ // Y ya está: qué cuenta queda puesta lo resuelve el ARRANQUE al recargar, que es donde
394
+ // esa decisión ya vivía —si no queda ninguna estrena la primera, y si quedan cae a la
395
+ // primera de la lista—. No hace falta decidirlo aquí también.
396
+ emitVault({ phase: 'account-removed', removed: gone, current: currentPid })
397
+ }
398
+
399
+ /**
400
+ * Borra un perfil y todo lo suyo. Sin preguntas: el freno de «no te quedes sin ninguna»
401
+ * es de la interfaz y vive en `deleteProfile`.
402
+ */
403
+ async function purgeProfile (id) {
404
+ const list = loadProfiles().filter((p) => p.id !== id)
405
+ saveProfiles(list)
406
+ for (const s of ['keypair', 'enc-keypair', 'me', 'nonces', 'delegations', 'revocations', 'vault.device', 'vault.cert', 'acta', 'renounced']) {
407
+ rawKv.removeItem(`dotrino.identity.p.${id}.${s}`)
408
+ }
409
+ // …y sus CryptoKeys no extractables del keyStore (IndexedDB).
410
+ if (keyStore) {
411
+ for (const s of ['keypair', 'enc-keypair']) {
412
+ try { await keyStore.remove(`dotrino.identity.p.${id}.${s}`) } catch (_) {}
413
+ }
414
+ }
415
+ if (currentPid === id) {
416
+ if (list.length) { currentPid = list[0].id; rawKv.setItem(CURRENT_STORAGE, currentPid) }
417
+ else {
418
+ // No queda ninguna: el arranque estrenará la primera. Se borra el puntero, pero
419
+ // `currentPid` se deja como está a propósito — sin él, el kv deja de estar
420
+ // scopeado y cualquier escritura de aquí a la recarga caería en las claves SIN
421
+ // namespace, que son justo las que el arranque adopta como «Perfil 1». Apuntando
422
+ // a un perfil que ya no existe, lo que se escriba es inerte.
423
+ rawKv.removeItem(CURRENT_STORAGE)
424
+ }
425
+ }
426
+ return { ok: true, current: currentPid }
399
427
  }
400
428
 
401
429
  /**
@@ -1255,22 +1283,13 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1255
1283
  return { id: e.id, name: e.name }
1256
1284
  },
1257
1285
  async deleteProfile ({ id } = {}) {
1258
- let list = loadProfiles()
1286
+ const list = loadProfiles()
1287
+ // El freno es de la INTERFAZ: el botón «Borrar» de la página de perfiles no puede
1288
+ // dejarte sin ninguna de un clic. La expulsión no pasa por aquí (ver `purgeProfile`):
1289
+ // ahí sí se va la última, porque no es un descuido sino que te echaron.
1259
1290
  if (list.length <= 1) throw new Error('cannot delete the only profile')
1260
1291
  if (!list.find((p) => p.id === id)) throw new Error('perfil no existe')
1261
- list = list.filter((p) => p.id !== id); saveProfiles(list)
1262
- // Borrado directo del namespace del perfil (incluye su store del vault si lo tuviera).
1263
- for (const s of ['keypair', 'enc-keypair', 'me', 'nonces', 'delegations', 'revocations', 'vault.device', 'vault.cert', 'acta', 'renounced']) {
1264
- rawKv.removeItem(`dotrino.identity.p.${id}.${s}`)
1265
- }
1266
- // …y sus CryptoKeys no extractables del keyStore (IndexedDB).
1267
- if (keyStore) {
1268
- for (const s of ['keypair', 'enc-keypair']) {
1269
- try { await keyStore.remove(`dotrino.identity.p.${id}.${s}`) } catch (_) {}
1270
- }
1271
- }
1272
- if (currentPid === id) { currentPid = list[0].id; rawKv.setItem(CURRENT_STORAGE, currentPid) }
1273
- return { ok: true, current: currentPid }
1292
+ return purgeProfile(id)
1274
1293
  },
1275
1294
 
1276
1295
  // ----- ACTA DE PERFIL -----