@dotrino/identity 0.88.0 → 0.89.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.88.0",
3
+ "version": "0.89.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,10 +167,16 @@ 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`). */
170
+ /**
171
+ * Pedidos de aprobación de la cuenta activa (o de otra, con `profile`).
172
+ *
173
+ * Cada pedido trae `ctx` —qué comando está pidiendo las claves y desde qué carpeta— ya
174
+ * abierto: viaja sellado a la llave de cifrado de este aparato y se descifra aquí dentro.
175
+ * `ctxError` si no se pudo abrir; sin ninguno de los dos, el pedido no dijo qué corría.
176
+ */
171
177
  vaultApprovals (op: 'approvals' | 'approve' | 'deny', args?: { id?: string; profile?: string }): Promise<any>
172
178
  /** 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 }>>
179
+ vaultApprovalsAll (): Promise<Array<{ profile: string; name: string; current: boolean; items: ApprovalRequest[]; error?: string }>>
174
180
  canApproveVault (): Promise<boolean>
175
181
  getVaultCert (): Promise<any>
176
182
  onVault (handler: (payload: any) => void): () => void
@@ -323,3 +329,33 @@ export function signSession (args: { sid: string; s: string; by: string; origin:
323
329
  export function verifySession (paper: SessionPaper, opts: { chain: any[]; expectedProfileId?: string | null; origin?: string | null; now?: number; maxSkewMs?: number }): Promise<VerifiedSession>
324
330
  /** ¿Firmó esta sesión esto, y su papel lo cubría? */
325
331
  export function verifySessionSigned (args: { data: any; signature: string; session: SessionPaper; chain: any[]; scope?: SessionScope | null; origin?: string | null; expectedProfileId?: string | null; now?: number }): Promise<VerifiedSession>
332
+
333
+ /** Un pedido de aprobación tal y como lo ve la pantalla que dice que sí o que no. */
334
+ export interface ApprovalRequest {
335
+ id: string
336
+ ns: string
337
+ deviceId: string | null
338
+ label: string
339
+ ts: number
340
+ exp: number
341
+ /** Qué comando está pidiendo las claves, ya descifrado. `null`/ausente si no lo dijo. */
342
+ ctx?: ProcessContext | null
343
+ /** Por qué no se pudo abrir el comando (`no-key`, `profile-locked`, `cannot-open`). */
344
+ ctxError?: string
345
+ /** La bóveda no pudo sellarlo para este aparato (`no-enc-key`, `seal-failed`). */
346
+ ctxSealed?: boolean
347
+ ctxReason?: string
348
+ }
349
+
350
+ /** Qué proceso pide, y si lo comprobó el kernel (`proc`) o solo lo dice él (`declared`). */
351
+ export interface ProcessContext {
352
+ pid: number | null
353
+ uid: number | null
354
+ exe: string
355
+ cwd: string
356
+ argv: string[]
357
+ truncated: boolean
358
+ user: string
359
+ host: string
360
+ verified: 'proc' | 'declared'
361
+ }
package/vault/core.js CHANGED
@@ -563,6 +563,66 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
563
563
  throw Object.assign(new Error('no signing key stored for that profile'), { code: 'no-key' })
564
564
  }
565
565
 
566
+ /**
567
+ * LA LLAVE DE CIFRADO DE OTRA CUENTA DE ESTE DISPOSITIVO, sin abrirla ni ponerla activa.
568
+ *
569
+ * El par de `signerForProfile`, y por el mismo motivo: la pantalla de Pedidos enseña los
570
+ * de TODAS las cuentas a la vez, y desde 2026-09-11 cada pedido trae **qué comando está
571
+ * pidiendo las claves** dentro de un sobre cerrado a la llave de ESA cuenta. Sin esto, los
572
+ * pedidos de las demás cuentas se verían sin comando — que es justo el dato por el que se
573
+ * mira la pantalla.
574
+ *
575
+ * Mismas tres reglas que allí: no se mueve `currentPid`, no se genera ninguna llave (si no
576
+ * está, se dice) y una cuenta sellada bajo su contraseña se contesta `profile-locked`.
577
+ */
578
+ async function encKeyForProfile (pid) {
579
+ if (pid === currentPid) return encKeypair?.privateKey || null
580
+ const { algo, privUses } = ALGO_OF.enc
581
+ const nombre = ENC_KEY_STORAGE.replace(/^dotrino\.identity\./, `dotrino.identity.p.${pid}.`)
582
+ if (keyStore) {
583
+ const guardado = await keyStore.get(nombre).catch(() => null)
584
+ if (guardado?.privateKey) return guardado.privateKey
585
+ }
586
+ const raw = rawKv.getItem(nombre)
587
+ if (raw) {
588
+ const g = JSON.parse(raw)
589
+ if (g?.sealed) throw Object.assign(new Error('that profile is locked: its encryption key is sealed under its password'), { code: 'profile-locked' })
590
+ if (g?.privateJwk) return crypto.subtle.importKey('jwk', g.privateJwk, algo, true, privUses)
591
+ }
592
+ throw Object.assign(new Error('no encryption key stored for that profile'), { code: 'no-key' })
593
+ }
594
+
595
+ /**
596
+ * ABRE EL COMANDO DE CADA PEDIDO, aquí dentro, donde están las llaves.
597
+ *
598
+ * La bóveda manda el comando y el path sellados a la llave de cifrado del aparato que
599
+ * pregunta (no los manda en claro: el camino hasta aquí es el proxio, que no cifra). Se
600
+ * abren en el iframe y salen a la página ya legibles: la página no ve ninguna llave, y el
601
+ * único tramo en claro es el `postMessage` entre dos ventanas del mismo navegador.
602
+ *
603
+ * Lo que no se puede abrir se dice (`ctxError`) en vez de quedarse como un pedido sin
604
+ * comando: son cosas distintas y en la pantalla hay que poder distinguirlas.
605
+ */
606
+ async function abrirContextos (items, pid) {
607
+ if (!Array.isArray(items) || !items.length) return items
608
+ let priv = null
609
+ let fallo = null
610
+ try { priv = await encKeyForProfile(pid) } catch (e) { fallo = e?.code || 'no-key' }
611
+ const salida = []
612
+ for (const raw of items) {
613
+ const { ctxWrap, ctxEnvelope, ...it } = raw || {}
614
+ if (!ctxWrap || !ctxEnvelope) { salida.push(it); continue }
615
+ if (!priv) { salida.push({ ...it, ctxError: fallo || 'no-key' }); continue }
616
+ try {
617
+ const cek = await Content.openWrap({ wrap: ctxWrap, myEncPrivateKey: priv })
618
+ salida.push({ ...it, ctx: JSON.parse(await Content.decryptWithCek({ cek, envelope: ctxEnvelope })) })
619
+ } catch (e) {
620
+ salida.push({ ...it, ctxError: e?.code || 'cannot-open' })
621
+ }
622
+ }
623
+ return salida
624
+ }
625
+
566
626
  /** La cuenta de este dispositivo que YA está emparejada con la bóveda `master`, si la hay. */
567
627
  const profilePairedWith = (master) => {
568
628
  if (!master) return null
@@ -2565,8 +2625,13 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
2565
2625
  const v = loadVaultCert(); const device = loadVaultDevice()
2566
2626
  if (!v?.cert || !device) throw new Error('this device is not paired with a vault')
2567
2627
  maybeRenewVaultCert()
2568
- try { return await remoteApproval({ master: v.master, proxy: v.proxy, device, cert: v.cert, op, id, onRevoked: wipeVaultLink }) }
2569
- catch (e) { return handleVaultError(e) }
2628
+ try {
2629
+ const r = await remoteApproval({ master: v.master, proxy: v.proxy, device, cert: v.cert, op, id, onRevoked: wipeVaultLink })
2630
+ // El comando de cada pedido viene sellado a este aparato: se abre aquí, que es
2631
+ // donde está la llave, y sale ya legible.
2632
+ if (Array.isArray(r?.items)) return { ...r, items: await abrirContextos(r.items, currentPid) }
2633
+ return r
2634
+ } catch (e) { return handleVaultError(e) }
2570
2635
  },
2571
2636
 
2572
2637
  /**
@@ -2607,7 +2672,8 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
2607
2672
  if (!device) return { ...base, items: [], error: 'no-key' }
2608
2673
  try {
2609
2674
  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 : [] }
2675
+ const items = Array.isArray(r?.items) ? await abrirContextos(r.items, p.id) : []
2676
+ return { ...base, items }
2611
2677
  } catch (e) {
2612
2678
  return { ...base, items: [], error: e?.code || e?.message || 'error' }
2613
2679
  }
@@ -1,4 +1,4 @@
1
- Copia vendorizada de @dotrino/proxy-client@0.18.2 (dotrino-proxy-client/src/{index,client,signature,canonical,sealing,webrtc}.js).
1
+ Copia vendorizada de @dotrino/proxy-client@0.19.0 (dotrino-proxy-client/src/{index,client,signature,canonical,sealing,webrtc}.js).
2
2
  NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
3
3
  sealing.js resuelve @dotrino/identity/content de forma PEREZOSA (= ../../content.js
4
4
  por el import map): solo se carga si de verdad se sella algo.
@@ -1,4 +1,4 @@
1
- Copia vendorizada de @dotrino/vault@0.62.1 (dotrino-vault/lib/src/{index,enroll,protocol}.js).
1
+ Copia vendorizada de @dotrino/vault@0.63.0 (dotrino-vault/lib/src/{index,enroll,protocol}.js).
2
2
  NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
3
3
  index.js importa ./enroll.js y ./protocol.js (relativos, van en esta misma copia),
4
4
  @dotrino/identity/{capabilities,acta} (= ../../{capabilities,acta}.js) y