@dotrino/identity 0.108.0 → 0.109.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.108.0",
3
+ "version": "0.109.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
@@ -187,9 +187,9 @@ export class Identity {
187
187
  * abierto: viaja sellado a la llave de cifrado de este aparato y se descifra aquí dentro.
188
188
  * `ctxError` si no se pudo abrir; sin ninguno de los dos, el pedido no dijo qué corría.
189
189
  */
190
- vaultApprovals (op: 'approvals' | 'approve' | 'deny', args?: { id?: string; profile?: string }): Promise<any>
190
+ vaultApprovals (op: 'approvals' | 'approve' | 'deny', args?: { id?: string; profile?: string; notify?: boolean }): Promise<any>
191
191
  /** Los pedidos de TODAS las cuentas de este dispositivo que aprueban, sin cambiar la activa. */
192
- vaultApprovalsAll (): Promise<Array<{ profile: string; name: string; current: boolean; items: ApprovalRequest[]; error?: string }>>
192
+ vaultApprovalsAll (args?: { notify?: boolean }): Promise<Array<{ profile: string; name: string; current: boolean; items: ApprovalRequest[]; error?: string }>>
193
193
  canApproveVault (): Promise<boolean>
194
194
  getVaultCert (): Promise<any>
195
195
  onVault (handler: (payload: any) => void): () => void
package/src/index.js CHANGED
@@ -476,6 +476,10 @@ export class Identity {
476
476
  * PEDIDOS DE APROBACIÓN de la bóveda (cajones con `approval`): `op` = `approvals` ·
477
477
  * `approve` · `deny` (estos dos con `{ id }`). Requiere `vault:approve` en el cert.
478
478
  *
479
+ * `args.notify` (solo con `approvals`): `true`/`false` según a este aparato le lleguen los
480
+ * avisos de pedidos. La bóveda lo apunta, y para actualizarse solo pide permiso si hay un
481
+ * aprobador que pueda enterarse. No pasarlo no cambia lo ya dicho.
482
+ *
479
483
  * `args.profile` apunta a OTRA cuenta de este dispositivo (la que diga
480
484
  * `vaultApprovalsAll`); sin él es la activa.
481
485
  */
@@ -494,8 +498,8 @@ export class Identity {
494
498
  *
495
499
  * Aprobar uno de otra cuenta: `vaultApprovals('approve', { id, profile })`.
496
500
  */
497
- async vaultApprovalsAll () {
498
- return this._call('vaultApprovalsAll', {}, 30000)
501
+ async vaultApprovalsAll (args) {
502
+ return this._call('vaultApprovalsAll', args || {}, 30000)
499
503
  }
500
504
 
501
505
  /** Registra el token de push de la app nativa (FCM/APNs) bajo la llave de este aparato. */
package/src/node.js CHANGED
@@ -263,7 +263,7 @@ export class Identity {
263
263
  getVaultCert () { return this._h('getVaultCert') }
264
264
  vaultApprovals (op, args) { return this._h('vaultApprovals', { op, ...(args || {}) }) }
265
265
  /** Los pedidos de TODAS las cuentas que aprueban, sin cambiar la activa. */
266
- vaultApprovalsAll () { return this._h('vaultApprovalsAll') }
266
+ vaultApprovalsAll (args) { return this._h('vaultApprovalsAll', args || {}) }
267
267
  onVault (handler) { return this.on('vault', handler) }
268
268
  // Multi-perfil por dispositivo (crear/cambiar reinicializa con el nuevo perfil activo).
269
269
  listProfiles () { return this._h('listProfiles') }
package/vault/core.js CHANGED
@@ -3057,12 +3057,12 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
3057
3057
  * `profile` apunta a OTRA cuenta de este mismo dispositivo (ver `vaultApprovalsAll`):
3058
3058
  * sin él es la activa, como siempre.
3059
3059
  */
3060
- async vaultApprovals ({ op, id, profile = null } = {}) {
3060
+ async vaultApprovals ({ op, id, profile = null, notify } = {}) {
3061
3061
  if (profile && profile !== currentPid) return approvalsDeOtroPerfil(profile, { op, id })
3062
3062
  const v = loadVaultCert(); const device = loadVaultDevice()
3063
3063
  if (!v?.cert || !device) throw new Error('this device is not paired with a vault')
3064
3064
  maybeRenewVaultCert()
3065
- const ask = (cert) => remoteApproval({ master: v.master, proxy: v.proxy, device, cert, op, id, onRevoked: wipeVaultLink })
3065
+ const ask = (cert) => remoteApproval({ master: v.master, proxy: v.proxy, device, cert, op, id, notify, onRevoked: wipeVaultLink })
3066
3066
  try {
3067
3067
  let r
3068
3068
  try { r = await ask(v.cert) } catch (e) {
@@ -3104,7 +3104,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
3104
3104
  * @returns {Promise<Array<{ profile, name, items, error? }>>} una entrada por cuenta que
3105
3105
  * aprueba — con sus pedidos, o con el motivo por el que no se pudo preguntar.
3106
3106
  */
3107
- async vaultApprovalsAll () {
3107
+ async vaultApprovalsAll ({ notify } = {}) {
3108
3108
  // La activa sí se pone al día: es la única en la que esta pantalla puede escribir.
3109
3109
  try { maybeRenewVaultCert() } catch (_) {}
3110
3110
  const perfiles = loadProfiles()
@@ -3119,7 +3119,9 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
3119
3119
  }
3120
3120
  if (!device) return { ...base, items: [], error: 'no-key' }
3121
3121
  try {
3122
- const r = await remoteApproval({ master: v.master, proxy: v.proxy, device, cert: v.cert, op: 'approvals' })
3122
+ // `notify` habla de la cuenta ACTIVA: los avisos se suscriben bajo SU llave. De las
3123
+ // demás no se dice nada, que no es lo mismo que decir que no.
3124
+ const r = await remoteApproval({ master: v.master, proxy: v.proxy, device, cert: v.cert, op: 'approvals', ...(p.id === currentPid ? { notify } : {}) })
3123
3125
  const items = Array.isArray(r?.items) ? await abrirContextos(r.items, p.id) : []
3124
3126
  return { ...base, items }
3125
3127
  } catch (e) {
package/vault/remote.js CHANGED
@@ -452,11 +452,16 @@ export async function requestAdmin ({ master, proxy, device, cert, op, onRevoked
452
452
  * `vault:approve` — que, como `admin`, no se recibe al emparejar: se concede a mano.
453
453
  * La bóveda contesta un cuerpo firmado por la maestra (`{ op, items | ok }`).
454
454
  */
455
- export async function requestApproval ({ master, proxy, device, cert, op, id, onRevoked } = {}) {
455
+ export async function requestApproval ({ master, proxy, device, cert, op, id, notify, onRevoked } = {}) {
456
+ // `notify`: al pedir la lista, este aparato dice si le llegan los avisos. La bóveda lo
457
+ // apunta y es lo que mira para saber si hay a quién pedirle permiso antes de actualizarse.
458
+ // Solo va si quien llama lo SABE (true o false): no decir nada no cambia lo ya dicho.
459
+ const data = id ? { op, id } : { op }
460
+ if (op === 'approvals' && typeof notify === 'boolean') data.notify = notify
456
461
  const res = await vaultRpc({
457
462
  master, proxy, device, cert, onRevoked,
458
463
  sendType: MSG.SECRETS, okType: MSG.SECRETS_RESULT,
459
- data: id ? { op, id } : { op }
464
+ data
460
465
  })
461
466
  return res.body
462
467
  }
@@ -1,4 +1,4 @@
1
- Copia vendorizada de @dotrino/proxy-client@0.28.0 (dotrino-proxy-client/src/{index,client,signature,canonical,sealing,encpub,webrtc,stats}.js).
1
+ Copia vendorizada de @dotrino/proxy-client@0.30.1 (dotrino-proxy-client/src/{index,client,signature,canonical,sealing,encpub,webrtc,stats,stats-log}.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.
@@ -545,6 +545,27 @@ export class WebSocketProxyClient {
545
545
  this.send(tokens, sobre)
546
546
  }
547
547
 
548
+ /**
549
+ * Mandar POR TOKEN algo que la app YA CIFRÓ por su cuenta (un canal de sesión propio,
550
+ * como el de `@dotrino/remote-agent`), con la misma red que `sendSealedTo`: sube al
551
+ * canal directo si lo hay, y si el token murió el mismo mensaje sale por `peerPubkey`
552
+ * y se avisa con `token_gone`.
553
+ *
554
+ * Existe porque `sendByPubkey` NUNCA sube de escalón —la cola es del proxio— y `send`
555
+ * a secas pierde en silencio lo que va a un token que ya no existe. Con `requireSealed`
556
+ * se niega igual que los demás: esto no abre una puerta para ir en claro.
557
+ *
558
+ * @param {string} token
559
+ * @param {any} payload
560
+ * @param {{ peerPubkey:string }} opts
561
+ */
562
+ sendToOrQueue (token, payload, { peerPubkey } = /** @type {any} */ ({})) {
563
+ if (!token || !peerPubkey) {
564
+ throw errorCon('sendToOrQueue: needs the token and whose it is (peerPubkey)', 'no-peer-identity')
565
+ }
566
+ this._sendToTokenOrQueue(token, payload, peerPubkey)
567
+ }
568
+
548
569
  /**
549
570
  * UN TOKEN MUERTO NO SE TRAGA EL MENSAJE. Un token es una conexión, y cuando la otra punta
550
571
  * reinicia la app su token deja de existir; la app que lo tenía apuntado sigue mandando ahí
@@ -11,6 +11,7 @@ export {
11
11
  } from './encpub.js'
12
12
 
13
13
  export { listTransports, routeOf } from './stats.js'
14
+ export { logStats, formatStats } from './stats-log.js'
14
15
 
15
16
  import { WebSocketProxyClient } from './client.js'
16
17
 
@@ -0,0 +1,96 @@
1
+ /**
2
+ * LAS ESTADÍSTICAS DE RED, AL LOG. Para lo que no tiene pantalla: un daemon, un agente, un
3
+ * servicio bajo pm2.
4
+ *
5
+ * En el navegador las enseña el topbar (su modal de red lee el registro de clientes). Un
6
+ * proceso de Node no tiene dónde, así que «¿esto va por WebRTC o sigue dando la vuelta por
7
+ * el proxio?» solo se contestaba con una prueba en contenedores. Esto escribe lo mismo que
8
+ * pinta el modal, una línea cada tanto, con el `log` que el servicio ya usa.
9
+ *
10
+ * Lee el MISMO registro que el topbar (`listTransports`), así que el servicio no le pasa
11
+ * sus clientes: los que haya en el proceso, salen.
12
+ *
13
+ * Tres cosas a propósito:
14
+ * - Solo escribe si algo cambió. Un servicio quieto no llena el log de líneas iguales.
15
+ * - Los contadores son ACUMULADOS desde `since`: dos líneas se restan, y una sola ya dice
16
+ * si hubo tráfico directo alguna vez.
17
+ * - De la otra punta solo sale el TOKEN, que es efímero. La pubkey no: con quién habla
18
+ * una máquina es justo lo que no se deja escrito en un disco.
19
+ */
20
+ import { listTransports, PATHS } from './stats.js'
21
+
22
+ const sum = (peers, dir) => {
23
+ const t = { proxy: 0, direct: 0, turn: 0, webrtc: 0 }
24
+ for (const p of peers) for (const k of PATHS) t[k] += p[dir][k]
25
+ return t
26
+ }
27
+
28
+ const pairs = (prefix, o) => PATHS.map((k) => `${prefix}.${k}=${o[k]}`).join(' ')
29
+
30
+ /**
31
+ * Las líneas de UN cliente, a partir de lo que devuelve `client.stats()`.
32
+ * La primera es el resumen; después, una por conexión.
33
+ *
34
+ * @param {object} s lo que devuelve `client.stats()` (`TransportStats` en los tipos)
35
+ * @param {{ label?: string }} [opts]
36
+ * @returns {string[]}
37
+ */
38
+ export function formatStats (s, opts = {}) {
39
+ const tag = opts.label ? `[net] ${opts.label}` : '[net]'
40
+ const routes = {}
41
+ for (const p of s.peers) routes[p.route] = (routes[p.route] || 0) + 1
42
+ const routeList = Object.keys(routes).sort().map((r) => `${r}=${routes[r]}`).join(',') || 'none'
43
+ const lines = [
44
+ `${tag} url=${s.url} connected=${s.connected ? 'yes' : 'no'} webrtc=${s.webrtc ? 'on' : 'off'}` +
45
+ ` ws.in=${s.proxy.bytesIn} ws.out=${s.proxy.bytesOut}` +
46
+ ` ${pairs('in', sum(s.peers, 'bytesIn'))} ${pairs('out', sum(s.peers, 'bytesOut'))}` +
47
+ ` peers=${s.peers.length} routes=${routeList}`
48
+ ]
49
+ for (const p of s.peers) {
50
+ lines.push(
51
+ `${tag} peer=${p.token || 'by-key'} route=${p.route}` +
52
+ ` ${pairs('in', p.bytesIn)} ${pairs('out', p.bytesOut)} msgs.in=${p.msgsIn} msgs.out=${p.msgsOut}`
53
+ )
54
+ }
55
+ return lines
56
+ }
57
+
58
+ /**
59
+ * Escribe las estadísticas de todos los clientes del proceso cada `everyMs`, si cambiaron.
60
+ *
61
+ * @param {{ log?: (line: string) => void, everyMs?: number, label?: string }} [opts]
62
+ * @returns {{ stop: () => Promise<void>, flush: () => Promise<void> }}
63
+ * `flush` escribe ya (si cambió algo); `stop` escribe una última vez y para.
64
+ */
65
+ export function logStats (opts = {}) {
66
+ const log = opts.log || ((line) => console.log(line))
67
+ const everyMs = opts.everyMs || 5 * 60 * 1000
68
+ /** cliente → lo último que se escribió de él */
69
+ const last = new WeakMap()
70
+
71
+ const flush = async () => {
72
+ for (const client of listTransports()) {
73
+ if (typeof client.stats !== 'function') continue
74
+ let lines
75
+ try {
76
+ lines = formatStats(await client.stats(), opts)
77
+ } catch (e) {
78
+ // No poder leer NO es «no hubo tráfico»: se dice, para que no parezca silencio.
79
+ lines = [`${opts.label ? `[net] ${opts.label}` : '[net]'} could not read stats: ${e?.message || e}`]
80
+ }
81
+ const text = lines.join('\n')
82
+ if (last.get(client) === text) continue
83
+ last.set(client, text)
84
+ for (const line of lines) log(line)
85
+ }
86
+ }
87
+
88
+ const timer = setInterval(() => { flush() }, everyMs)
89
+ // El log no puede ser lo que mantiene vivo un proceso que ya terminó.
90
+ if (typeof timer.unref === 'function') timer.unref()
91
+
92
+ return {
93
+ flush,
94
+ stop: async () => { clearInterval(timer); await flush() }
95
+ }
96
+ }
@@ -1,4 +1,4 @@
1
- Copia vendorizada de @dotrino/vault@0.79.0 (dotrino-vault/lib/src/{index,enroll,protocol,passwordLogins,loginClient,b64}.js).
1
+ Copia vendorizada de @dotrino/vault@0.80.0 (dotrino-vault/lib/src/{index,enroll,protocol,passwordLogins,loginClient,b64}.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
@@ -103,6 +103,13 @@ export const MSG = Object.freeze({
103
103
  // una forma de echar a alguien de su cuenta.
104
104
  LOGIN_CLOSE: 'vault.login.close', // aparato → vault: { data:{op,user,sid,publickey,ts}, signature }
105
105
  LOGIN_CLOSED: 'vault.login.closed', // vault → aparato: { ok }
106
+ // INCIDENTE: un agente de la cuenta (la terminal) avisa de que OTRO aparato de la cuenta
107
+ // se equivocó de clave tres veces seguidas. La bóveda lo apunta como un pedido más
108
+ // (`kind: 'incident'`), timbra a quien aprueba, y el aprobador bloquea o ignora. Bloquear
109
+ // no toca el acta: es una lista de la bóveda (`blocked.json`) que viaja a los agentes con
110
+ // `vault.devices.result` (`blocked`) y se deshace con `dotrino-vault unblock <ID>`.
111
+ INCIDENT: 'vault.incident', // agente → vault: { data:{op:'incident',kind,about,tries,publickey,ts}, signature, cert }
112
+ INCIDENT_RESULT: 'vault.incident.result', // vault → agente: { id, approvers, blocked }
106
113
  ERROR: 'vault.error' // vault → dispositivo: { error }
107
114
  })
108
115