@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 +1 -1
- package/src/index.d.ts +2 -2
- package/src/index.js +6 -2
- package/src/node.js +1 -1
- package/vault/core.js +6 -4
- package/vault/remote.js +7 -2
- package/vault/vendor/proxy-client/VERSION.txt +1 -1
- package/vault/vendor/proxy-client/client.js +21 -0
- package/vault/vendor/proxy-client/index.js +1 -0
- package/vault/vendor/proxy-client/stats-log.js +96 -0
- package/vault/vendor/vault/VERSION.txt +1 -1
- package/vault/vendor/vault/protocol.js +7 -0
package/package.json
CHANGED
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
|
-
|
|
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
|
|
464
|
+
data
|
|
460
465
|
})
|
|
461
466
|
return res.body
|
|
462
467
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/proxy-client@0.
|
|
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í
|
|
@@ -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.
|
|
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
|
|