@dotrino/identity 0.87.0 → 0.88.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.87.0",
3
+ "version": "0.88.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/index.d.ts CHANGED
@@ -167,6 +167,11 @@ export class Identity {
167
167
  vaultSign (payload: any): Promise<{ signature: string; publickey: string }>
168
168
  vaultStore (method: string, args?: any): Promise<any>
169
169
  listVaultDevices (): Promise<{ devices: any[]; revoked: any[] }>
170
+ /** Pedidos de aprobación de la cuenta activa (o de otra, con `profile`). */
171
+ vaultApprovals (op: 'approvals' | 'approve' | 'deny', args?: { id?: string; profile?: string }): Promise<any>
172
+ /** Los pedidos de TODAS las cuentas de este dispositivo que aprueban, sin cambiar la activa. */
173
+ vaultApprovalsAll (): Promise<Array<{ profile: string; name: string; current: boolean; items: any[]; error?: string }>>
174
+ canApproveVault (): Promise<boolean>
170
175
  getVaultCert (): Promise<any>
171
176
  onVault (handler: (payload: any) => void): () => void
172
177
  // self-vault (este dispositivo ES el vault, daemon dentro del iframe)
package/src/index.js CHANGED
@@ -463,11 +463,29 @@ export class Identity {
463
463
  /**
464
464
  * PEDIDOS DE APROBACIÓN de la bóveda (cajones con `approval`): `op` = `approvals` ·
465
465
  * `approve` · `deny` (estos dos con `{ id }`). Requiere `vault:approve` en el cert.
466
+ *
467
+ * `args.profile` apunta a OTRA cuenta de este dispositivo (la que diga
468
+ * `vaultApprovalsAll`); sin él es la activa.
466
469
  */
467
470
  async vaultApprovals (op, args) {
468
471
  return this._call('vaultApprovals', { op, ...(args || {}) }, 20000)
469
472
  }
470
473
 
474
+ /**
475
+ * LOS PEDIDOS DE TODAS TUS CUENTAS, SIN CAMBIARTE DE CUENTA.
476
+ *
477
+ * Una entrada por cuenta de este dispositivo que pueda aprobar: `{ profile, name,
478
+ * current, items }`, o `{ ..., error }` si a esa no se le pudo preguntar. El timbre no
479
+ * dice a qué cuenta llamó, así que quien enseña Pedidos tiene que mirar en todas —y
480
+ * hacerlo cambiando la cuenta activa significaba una recarga por cuenta, con el avatar
481
+ * y el icono de la app cambiando a la vista.
482
+ *
483
+ * Aprobar uno de otra cuenta: `vaultApprovals('approve', { id, profile })`.
484
+ */
485
+ async vaultApprovalsAll () {
486
+ return this._call('vaultApprovalsAll', {}, 30000)
487
+ }
488
+
471
489
  /** Registra el token de push de la app nativa (FCM/APNs) bajo la llave de este aparato. */
472
490
  async registerPush (args) {
473
491
  return this._call('registerPush', args || {}, 20000)
package/src/node.js CHANGED
@@ -257,6 +257,9 @@ export class Identity {
257
257
  vaultStore (method, args) { return this._h('vaultStore', { method, args }) }
258
258
  listVaultDevices () { return this._h('listVaultDevices') }
259
259
  getVaultCert () { return this._h('getVaultCert') }
260
+ vaultApprovals (op, args) { return this._h('vaultApprovals', { op, ...(args || {}) }) }
261
+ /** Los pedidos de TODAS las cuentas que aprueban, sin cambiar la activa. */
262
+ vaultApprovalsAll () { return this._h('vaultApprovalsAll') }
260
263
  onVault (handler) { return this.on('vault', handler) }
261
264
  // Multi-perfil por dispositivo (crear/cambiar reinicializa con el nuevo perfil activo).
262
265
  listProfiles () { return this._h('listProfiles') }
package/vault/core.js CHANGED
@@ -527,6 +527,42 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
527
527
  } catch (_) { return null }
528
528
  }
529
529
 
530
+ /**
531
+ * LA LLAVE DE FIRMA DE OTRA CUENTA DE ESTE DISPOSITIVO, sin abrirla ni ponerla activa.
532
+ *
533
+ * Existe para poder PREGUNTAR por los pedidos de cada cuenta sin cambiarse a ella (ver
534
+ * `vaultApprovalsAll`). La privada nunca se ve: en el almacén de llaves es una `CryptoKey`
535
+ * no extraíble y se devuelve tal cual, para firmar y nada más.
536
+ *
537
+ * Tres cosas que NO se hacen aquí, y las tres importan:
538
+ *
539
+ * · **No se mueve `currentPid`.** Abrir otra cuenta «un momento» pisa la identidad en
540
+ * memoria y además persiste cuál es la activa: cualquier otra llamada que caiga en medio
541
+ * se atendería con la cuenta equivocada, y una recarga a mitad te deja en otra cuenta.
542
+ * · **No se genera ninguna llave.** Si no está, se dice. Un `catch` que cae a `generateKey`
543
+ * le cambiaría la identidad a esa cuenta y la dejaría fuera de su propio perfil para
544
+ * siempre — el mismo fallo que ya está avisado en `loadOrCreatePair`.
545
+ * · **No se abre lo sellado.** Una cuenta del camino legado guarda su privada sellada bajo
546
+ * su contraseña; sin la frase no hay con qué, y se contesta `profile-locked` en vez de
547
+ * fingir que no existe.
548
+ */
549
+ async function signerForProfile (pid) {
550
+ const nombre = KEY_STORAGE.replace(/^dotrino\.identity\./, `dotrino.identity.p.${pid}.`)
551
+ if (keyStore) {
552
+ const guardado = await keyStore.get(nombre).catch(() => null)
553
+ if (guardado?.privateKey && guardado?.publicJwk) {
554
+ return { publickey: JSON.stringify(guardado.publicJwk), privateKey: guardado.privateKey }
555
+ }
556
+ }
557
+ const raw = rawKv.getItem(nombre)
558
+ if (raw) {
559
+ const g = JSON.parse(raw)
560
+ if (g?.sealed) throw Object.assign(new Error('that profile is locked: its key is sealed under its password'), { code: 'profile-locked' })
561
+ if (g?.privateJwk && g?.publicJwk) return { publickey: JSON.stringify(g.publicJwk), privateJwk: g.privateJwk }
562
+ }
563
+ throw Object.assign(new Error('no signing key stored for that profile'), { code: 'no-key' })
564
+ }
565
+
530
566
  /** La cuenta de este dispositivo que YA está emparejada con la bóveda `master`, si la hay. */
531
567
  const profilePairedWith = (master) => {
532
568
  if (!master) return null
@@ -575,6 +611,25 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
575
611
  if (e && /\brevoked\b/.test(e.message || '')) emitVault({ phase: 'rejected', reason: e.message })
576
612
  throw e
577
613
  }
614
+ /**
615
+ * APROBAR O DENEGAR un pedido de OTRA cuenta de este dispositivo.
616
+ *
617
+ * Es el par de `vaultApprovalsAll`: si la pantalla enseña los pedidos de las tres cuentas,
618
+ * el botón «Aprobar» tiene que funcionar en las tres — si no, seguirías teniendo que
619
+ * cambiarte, que es lo que esto vino a quitar. Firma con la llave de ESA cuenta y no toca
620
+ * nada de la activa.
621
+ *
622
+ * Solo lo suyo: sin cert o sin `vault:approve` en ese cert, no hay nada que hacer aquí.
623
+ */
624
+ async function approvalsDeOtroPerfil (pid, { op, id } = {}) {
625
+ if (!loadProfiles().some((p) => p.id === pid)) throw Object.assign(new Error('that profile does not exist on this device'), { code: 'no-profile' })
626
+ const v = vaultCertOf(pid)
627
+ if (!v?.cert) throw Object.assign(new Error('that profile is not paired with a vault'), { code: 'not-paired' })
628
+ if (!(v.cert.scope || []).includes('vault:approve')) throw Object.assign(new Error('that profile does not approve requests'), { code: 'no-approve' })
629
+ const device = await signerForProfile(pid)
630
+ return remoteApproval({ master: v.master, proxy: v.proxy, device, cert: v.cert, op, id })
631
+ }
632
+
578
633
  /** Id estable y corto de una llave de cifrado: con esto se indexan las envolturas. */
579
634
  const encKeyId = async (encPub) => (await pubkeyIdOf(encPub)).slice(0, 16)
580
635
 
@@ -2501,8 +2556,12 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
2501
2556
  * PEDIDOS DE APROBACIÓN: lo que le toca al teléfono cuando un cajón de la bóveda exige
2502
2557
  * el visto bueno por uso. `op`: `approvals` (listar) · `approve` · `deny` (con `id`).
2503
2558
  * Requiere `vault:approve` en el cert, que se concede a mano (`caps <ID> +aprueba`).
2559
+ *
2560
+ * `profile` apunta a OTRA cuenta de este mismo dispositivo (ver `vaultApprovalsAll`):
2561
+ * sin él es la activa, como siempre.
2504
2562
  */
2505
- async vaultApprovals ({ op, id } = {}) {
2563
+ async vaultApprovals ({ op, id, profile = null } = {}) {
2564
+ if (profile && profile !== currentPid) return approvalsDeOtroPerfil(profile, { op, id })
2506
2565
  const v = loadVaultCert(); const device = loadVaultDevice()
2507
2566
  if (!v?.cert || !device) throw new Error('this device is not paired with a vault')
2508
2567
  maybeRenewVaultCert()
@@ -2510,6 +2569,52 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
2510
2569
  catch (e) { return handleVaultError(e) }
2511
2570
  },
2512
2571
 
2572
+ /**
2573
+ * LOS PEDIDOS DE TODAS TUS CUENTAS, SIN CAMBIARTE DE CUENTA.
2574
+ *
2575
+ * El timbre del teléfono no dice a qué perfil llamó —viaja por FCM, o sea por Google, y
2576
+ * ahí no se mete nada que identifique al dueño—, así que la pantalla de Pedidos tiene
2577
+ * que mirar en todos los que aprueban. Antes lo hacía cambiando el perfil activo y
2578
+ * recargando la página una vez por cuenta: funcionaba y era insufrible, porque el avatar
2579
+ * y el icono de la app cambiaban dos o tres veces por abrir la pantalla (dueño,
2580
+ * 2026-09-10: «rotan los perfiles, cambia el icono y es molesto»).
2581
+ *
2582
+ * Y era innecesario: **este iframe tiene las llaves de todos los perfiles**. Cada cuenta
2583
+ * guarda la suya en el mismo almacén, bajo su propio nombre, y son `CryptoKey` NO
2584
+ * EXTRAÍBLES: se puede firmar con ellas sin verlas y sin tocar cuál es la activa. Eso es
2585
+ * justo lo que hace falta, y es lo único que se hace aquí.
2586
+ *
2587
+ * Lo que NO hace, a propósito: no renueva certificados ajenos ni borra enlaces ajenos
2588
+ * (eso escribe, y escribir en otra cuenta desde la pantalla de otra es pedir un lío).
2589
+ * Mirar es de solo lectura; si el papel de una cuenta está desfasado, se dice y punto.
2590
+ *
2591
+ * @returns {Promise<Array<{ profile, name, items, error? }>>} una entrada por cuenta que
2592
+ * aprueba — con sus pedidos, o con el motivo por el que no se pudo preguntar.
2593
+ */
2594
+ async vaultApprovalsAll () {
2595
+ // La activa sí se pone al día: es la única en la que esta pantalla puede escribir.
2596
+ try { maybeRenewVaultCert() } catch (_) {}
2597
+ const perfiles = loadProfiles()
2598
+ const salida = await Promise.all(perfiles.map(async (p) => {
2599
+ const v = p.id === currentPid ? loadVaultCert() : vaultCertOf(p.id)
2600
+ if (!v?.cert) return null // sin bóveda: no es asunto suyo
2601
+ if (!(v.cert.scope || []).includes('vault:approve')) return null // no aprueba: tampoco
2602
+ const base = { profile: p.id, name: p.name || '', current: p.id === currentPid }
2603
+ let device = null
2604
+ try { device = p.id === currentPid ? loadVaultDevice() : await signerForProfile(p.id) } catch (e) {
2605
+ return { ...base, items: [], error: e?.code || 'no-key' }
2606
+ }
2607
+ if (!device) return { ...base, items: [], error: 'no-key' }
2608
+ try {
2609
+ const r = await remoteApproval({ master: v.master, proxy: v.proxy, device, cert: v.cert, op: 'approvals' })
2610
+ return { ...base, items: Array.isArray(r?.items) ? r.items : [] }
2611
+ } catch (e) {
2612
+ return { ...base, items: [], error: e?.code || e?.message || 'error' }
2613
+ }
2614
+ }))
2615
+ return salida.filter(Boolean)
2616
+ },
2617
+
2513
2618
  /**
2514
2619
  * PUSH DE LA APP NATIVA: registra el token (FCM/APNs) de este aparato en el proxio,
2515
2620
  * bajo la llave del dispositivo, para que la bóveda pueda «timbrarlo» cuando haya