@dotrino/identity 0.48.1 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/vault/core.js +51 -37
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/identity",
3
- "version": "0.48.1",
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/vault/core.js CHANGED
@@ -350,18 +350,20 @@ 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í.
365
367
  *
366
368
  * El paso 3 es SOLO del navegador (`removeAccountOnExpulsion`). En Node las cuentas las
367
369
  * lleva quien hospeda —el daemon del vault tiene su propio registro de perfiles, en
@@ -377,30 +379,51 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
377
379
  if (removeAccountOnExpulsion) removeThisAccount().catch(() => {})
378
380
  }
379
381
 
380
- /**
381
- * Borra la cuenta de ESTE dispositivo y deja otra activa (existente o recién creada).
382
- * Se usa al ser expulsado; ver `wipeVaultLink`.
383
- */
382
+ /** Borra la cuenta de ESTE dispositivo al ser expulsado; ver `wipeVaultLink`. */
384
383
  let removingAccount = false
385
384
  async function removeThisAccount () {
386
- // UNA VEZ. Puede haber varias peticiones en vuelo y a todas les llega el mismo aviso;
387
- // sin este cerrojo, la segunda borraría la cuenta RECIÉN CREADA (para entonces
388
- // `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.
389
386
  if (removingAccount) return
390
387
  removingAccount = true
391
388
  const gone = currentPid
392
- // Si esa era la única cuenta hay que estrenar otra ANTES de borrarla: `deleteProfile`
393
- // se niega a dejar el dispositivo sin ninguna, y con razón. Crear ya deja la nueva
394
- // activa; si había otras, es el propio `deleteProfile` quien pasa a la primera.
395
- const created = loadProfiles().filter((p) => p.id !== gone).length === 0
396
- try {
397
- if (created) await handlers.createProfile({ name: '' })
398
- await handlers.deleteProfile({ id: gone })
399
- } catch (e) {
400
- 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) })
401
391
  return
402
392
  }
403
- 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 }
404
427
  }
405
428
 
406
429
  /**
@@ -1260,22 +1283,13 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1260
1283
  return { id: e.id, name: e.name }
1261
1284
  },
1262
1285
  async deleteProfile ({ id } = {}) {
1263
- 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.
1264
1290
  if (list.length <= 1) throw new Error('cannot delete the only profile')
1265
1291
  if (!list.find((p) => p.id === id)) throw new Error('perfil no existe')
1266
- list = list.filter((p) => p.id !== id); saveProfiles(list)
1267
- // Borrado directo del namespace del perfil (incluye su store del vault si lo tuviera).
1268
- for (const s of ['keypair', 'enc-keypair', 'me', 'nonces', 'delegations', 'revocations', 'vault.device', 'vault.cert', 'acta', 'renounced']) {
1269
- rawKv.removeItem(`dotrino.identity.p.${id}.${s}`)
1270
- }
1271
- // …y sus CryptoKeys no extractables del keyStore (IndexedDB).
1272
- if (keyStore) {
1273
- for (const s of ['keypair', 'enc-keypair']) {
1274
- try { await keyStore.remove(`dotrino.identity.p.${id}.${s}`) } catch (_) {}
1275
- }
1276
- }
1277
- if (currentPid === id) { currentPid = list[0].id; rawKv.setItem(CURRENT_STORAGE, currentPid) }
1278
- return { ok: true, current: currentPid }
1292
+ return purgeProfile(id)
1279
1293
  },
1280
1294
 
1281
1295
  // ----- ACTA DE PERFIL -----