@dotrino/identity 0.108.0 → 0.110.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 +34 -7
- package/vault/externalKeys.js +15 -0
- package/vault/nativeStore.js +3 -2
- 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 +107 -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
|
@@ -1121,9 +1121,22 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
1121
1121
|
// se hace al generar una propia — quién puede hacer qué con esta llave lo decide esta
|
|
1122
1122
|
// casa, no una etiqueta que viajó dentro del paquete.
|
|
1123
1123
|
const sinTopes = (jwk) => { const { key_ops: _o, ...resto } = jwk || {}; return resto }
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1124
|
+
if (keyStore?.importPair) {
|
|
1125
|
+
// DENTRO DE UNA APP NATIVA: la llave que entrega la bóveda la guarda la app (cifrada
|
|
1126
|
+
// con una llave del chip) y firma ella. Van las dos mitades juntas, bajo un mismo id.
|
|
1127
|
+
if (!entrada.keys.enc || !entrada.encPublickey) {
|
|
1128
|
+
throw Object.assign(new Error('this login has no encryption key: the phone needs both'), { code: 'native-import-needs-enc' })
|
|
1129
|
+
}
|
|
1130
|
+
await keyStore.importPair({
|
|
1131
|
+
sign: { name: _scoped(KEY_STORAGE), privateJwk: sinTopes(entrada.keys.sign), publickey: entrada.publickey },
|
|
1132
|
+
enc: { name: _scoped(ENC_KEY_STORAGE), privateJwk: sinTopes(entrada.keys.enc), publickey: entrada.encPublickey }
|
|
1133
|
+
})
|
|
1134
|
+
kv.removeItem(KEY_STORAGE); kv.removeItem(ENC_KEY_STORAGE)
|
|
1135
|
+
} else {
|
|
1136
|
+
await adoptJwkPair('sign', KEY_STORAGE, sinTopes(entrada.keys.sign), JSON.parse(entrada.publickey))
|
|
1137
|
+
if (entrada.keys.enc && entrada.encPublickey) {
|
|
1138
|
+
await adoptJwkPair('enc', ENC_KEY_STORAGE, sinTopes(entrada.keys.enc), JSON.parse(entrada.encPublickey))
|
|
1139
|
+
}
|
|
1127
1140
|
}
|
|
1128
1141
|
saveActa(entrada.acta)
|
|
1129
1142
|
kv.setItem(ACTA_HISTORY_STORAGE, '[]')
|
|
@@ -1147,6 +1160,18 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
1147
1160
|
list.push({ id: pid, name: entrada.user, pubkey: publickeyJwkStr })
|
|
1148
1161
|
saveProfiles(list)
|
|
1149
1162
|
emitVault({ phase: 'login', address: entrada.address, volatile: !remember })
|
|
1163
|
+
// En una app nativa, la cuenta pasa también a su pantalla de Pedidos, con el mismo papel
|
|
1164
|
+
// (como al emparejar). Si la app no la guarda, se dice: la entrada en sí ya está hecha.
|
|
1165
|
+
if (keypair?.privateKey?.external) {
|
|
1166
|
+
keypair.privateKey.paired({
|
|
1167
|
+
cert: entrada.cert, vault: entrada.iss, proxy: proxy || DEFAULT_PROXY,
|
|
1168
|
+
deviceId: await deviceIdDe(entrada.publickey),
|
|
1169
|
+
name: entrada.acta?.name || entrada.address, profileId: entrada.acta?.profileId || null
|
|
1170
|
+
}).then(
|
|
1171
|
+
() => emitVault({ phase: 'native-account', ok: true }),
|
|
1172
|
+
(e) => { console.warn('[identity] the phone did not save the account: ' + e.message); emitVault({ phase: 'native-account', ok: false, code: e.code || 'native-error', reason: e.message }) }
|
|
1173
|
+
)
|
|
1174
|
+
}
|
|
1150
1175
|
return { id: pid, name: entrada.user, address: entrada.address, user: entrada.user, sid: entrada.sid, volatile: !remember }
|
|
1151
1176
|
} catch (e) {
|
|
1152
1177
|
// NADA DE MEDIAS CUENTAS: si algo falla, no se queda una identidad a medio adoptar en
|
|
@@ -3057,12 +3082,12 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
3057
3082
|
* `profile` apunta a OTRA cuenta de este mismo dispositivo (ver `vaultApprovalsAll`):
|
|
3058
3083
|
* sin él es la activa, como siempre.
|
|
3059
3084
|
*/
|
|
3060
|
-
async vaultApprovals ({ op, id, profile = null } = {}) {
|
|
3085
|
+
async vaultApprovals ({ op, id, profile = null, notify } = {}) {
|
|
3061
3086
|
if (profile && profile !== currentPid) return approvalsDeOtroPerfil(profile, { op, id })
|
|
3062
3087
|
const v = loadVaultCert(); const device = loadVaultDevice()
|
|
3063
3088
|
if (!v?.cert || !device) throw new Error('this device is not paired with a vault')
|
|
3064
3089
|
maybeRenewVaultCert()
|
|
3065
|
-
const ask = (cert) => remoteApproval({ master: v.master, proxy: v.proxy, device, cert, op, id, onRevoked: wipeVaultLink })
|
|
3090
|
+
const ask = (cert) => remoteApproval({ master: v.master, proxy: v.proxy, device, cert, op, id, notify, onRevoked: wipeVaultLink })
|
|
3066
3091
|
try {
|
|
3067
3092
|
let r
|
|
3068
3093
|
try { r = await ask(v.cert) } catch (e) {
|
|
@@ -3104,7 +3129,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
3104
3129
|
* @returns {Promise<Array<{ profile, name, items, error? }>>} una entrada por cuenta que
|
|
3105
3130
|
* aprueba — con sus pedidos, o con el motivo por el que no se pudo preguntar.
|
|
3106
3131
|
*/
|
|
3107
|
-
async vaultApprovalsAll () {
|
|
3132
|
+
async vaultApprovalsAll ({ notify } = {}) {
|
|
3108
3133
|
// La activa sí se pone al día: es la única en la que esta pantalla puede escribir.
|
|
3109
3134
|
try { maybeRenewVaultCert() } catch (_) {}
|
|
3110
3135
|
const perfiles = loadProfiles()
|
|
@@ -3119,7 +3144,9 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
3119
3144
|
}
|
|
3120
3145
|
if (!device) return { ...base, items: [], error: 'no-key' }
|
|
3121
3146
|
try {
|
|
3122
|
-
|
|
3147
|
+
// `notify` habla de la cuenta ACTIVA: los avisos se suscriben bajo SU llave. De las
|
|
3148
|
+
// demás no se dice nada, que no es lo mismo que decir que no.
|
|
3149
|
+
const r = await remoteApproval({ master: v.master, proxy: v.proxy, device, cert: v.cert, op: 'approvals', ...(p.id === currentPid ? { notify } : {}) })
|
|
3123
3150
|
const items = Array.isArray(r?.items) ? await abrirContextos(r.items, p.id) : []
|
|
3124
3151
|
return { ...base, items }
|
|
3125
3152
|
} catch (e) {
|
package/vault/externalKeys.js
CHANGED
|
@@ -78,6 +78,21 @@ export function withExternalKeys (keyStore, bridge) {
|
|
|
78
78
|
const other = sib ? await keyStore.get(sib) : null
|
|
79
79
|
if (rec?.external && !(other?.external === rec.external)) await bridge.call('remove', { kid: rec.external })
|
|
80
80
|
},
|
|
81
|
+
/**
|
|
82
|
+
* UNA LLAVE QUE YA EXISTE (entrar con usuario y contraseña): la bóveda la entrega y no se
|
|
83
|
+
* puede meter en el chip, así que la app la guarda cifrada con una llave del chip y firma
|
|
84
|
+
* ella. Desde aquí es una llave externa más: la página no se queda con la privada.
|
|
85
|
+
* `sign` y `enc`: `{ name, privateJwk, publickey }`, con la pública TAL CUAL la escribe el
|
|
86
|
+
* acta (es la cadena con la que esa llave es miembro).
|
|
87
|
+
*/
|
|
88
|
+
async importPair ({ sign, enc }) {
|
|
89
|
+
const k = await bridge.call('import', {
|
|
90
|
+
sign: sign.privateJwk, enc: enc.privateJwk, publickey: sign.publickey, encPub: enc.publickey
|
|
91
|
+
})
|
|
92
|
+
if (!k?.kid) throw Object.assign(new Error('the phone did not keep the key'), { code: 'native-no-key' })
|
|
93
|
+
await keyStore.set(sign.name, { external: k.kid, kind: 'sign', publicJwk: JSON.parse(sign.publickey) })
|
|
94
|
+
await keyStore.set(enc.name, { external: k.kid, kind: 'enc', publicJwk: JSON.parse(enc.publickey) })
|
|
95
|
+
},
|
|
81
96
|
async create (kind, name) {
|
|
82
97
|
// Las dos mitades del perfil van bajo la MISMA llave del chip: si la otra ya nació
|
|
83
98
|
// allí, se reutiliza; si no, se crea una nueva.
|
package/vault/nativeStore.js
CHANGED
|
@@ -28,8 +28,9 @@ function plainKeyRecord (rec) {
|
|
|
28
28
|
if (rec && typeof rec.external === 'string' && rec.publicJwk && (rec.kind === 'sign' || rec.kind === 'enc')) {
|
|
29
29
|
return { external: rec.external, kind: rec.kind, publicJwk: rec.publicJwk }
|
|
30
30
|
}
|
|
31
|
-
// Una llave de SOFTWARE
|
|
32
|
-
//
|
|
31
|
+
// Una llave de SOFTWARE no se guarda aquí: guardarla a medias sería perder la cuenta sin
|
|
32
|
+
// decirlo. La de entrar con contraseña no llega a este punto: la guarda la app, cifrada con
|
|
33
|
+
// una llave del chip (`importPair` de `externalKeys.js`), y aquí queda su registro externo.
|
|
33
34
|
throw Object.assign(new Error('this app keeps keys in the phone chip only: a software key cannot be stored here'), { code: 'native-no-import' })
|
|
34
35
|
}
|
|
35
36
|
|
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.4 (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,107 @@
|
|
|
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
|
+
// QUIÉN CONTESTÓ Y QUIÉN NO. Un sondeo (un ping a cada aparato del acta) deja una entrada
|
|
41
|
+
// por destinatario aunque nadie responda: esas no son conexiones y van resumidas
|
|
42
|
+
// (`unanswered`), igual que en el informe del topbar y de las apps nativas.
|
|
43
|
+
// `unknown`: quien nos escribió, nunca recibió respuesta y no sabemos su llave (otro aparato
|
|
44
|
+
// sondeando qué máquinas están encendidas). Tampoco es una conexión.
|
|
45
|
+
const isUnknown = (p) => p.msgsIn > 0 && !(p.msgsOut > 0) && !p.pubkey
|
|
46
|
+
const unknown = s.peers.filter(isUnknown)
|
|
47
|
+
const talking = s.peers.filter((p) => p.msgsIn > 0 && !isUnknown(p))
|
|
48
|
+
const silent = s.peers.filter((p) => !(p.msgsIn > 0))
|
|
49
|
+
const routes = {}
|
|
50
|
+
for (const p of talking) routes[p.route] = (routes[p.route] || 0) + 1
|
|
51
|
+
const routeList = Object.keys(routes).sort().map((r) => `${r}=${routes[r]}`).join(',') || 'none'
|
|
52
|
+
const lines = [
|
|
53
|
+
`${tag} url=${s.url} connected=${s.connected ? 'yes' : 'no'} webrtc=${s.webrtc ? 'on' : 'off'}` +
|
|
54
|
+
` ws.in=${s.proxy.bytesIn} ws.out=${s.proxy.bytesOut}` +
|
|
55
|
+
` ${pairs('in', sum(s.peers, 'bytesIn'))} ${pairs('out', sum(s.peers, 'bytesOut'))}` +
|
|
56
|
+
` peers=${talking.length} routes=${routeList}` +
|
|
57
|
+
(silent.length ? ` unanswered=${silent.length} unanswered.out=${silent.reduce((n, p) => n + PATHS.reduce((m, k) => m + p.bytesOut[k], 0), 0)}` : '') +
|
|
58
|
+
(unknown.length ? ` unknown=${unknown.length} unknown.in=${unknown.reduce((n, p) => n + PATHS.reduce((m, k) => m + p.bytesIn[k], 0), 0)}` : '')
|
|
59
|
+
]
|
|
60
|
+
for (const p of talking) {
|
|
61
|
+
lines.push(
|
|
62
|
+
`${tag} peer=${p.token || 'by-key'} route=${p.route}` +
|
|
63
|
+
` ${pairs('in', p.bytesIn)} ${pairs('out', p.bytesOut)} msgs.in=${p.msgsIn} msgs.out=${p.msgsOut}`
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
return lines
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Escribe las estadísticas de todos los clientes del proceso cada `everyMs`, si cambiaron.
|
|
71
|
+
*
|
|
72
|
+
* @param {{ log?: (line: string) => void, everyMs?: number, label?: string }} [opts]
|
|
73
|
+
* @returns {{ stop: () => Promise<void>, flush: () => Promise<void> }}
|
|
74
|
+
* `flush` escribe ya (si cambió algo); `stop` escribe una última vez y para.
|
|
75
|
+
*/
|
|
76
|
+
export function logStats (opts = {}) {
|
|
77
|
+
const log = opts.log || ((line) => console.log(line))
|
|
78
|
+
const everyMs = opts.everyMs || 5 * 60 * 1000
|
|
79
|
+
/** cliente → lo último que se escribió de él */
|
|
80
|
+
const last = new WeakMap()
|
|
81
|
+
|
|
82
|
+
const flush = async () => {
|
|
83
|
+
for (const client of listTransports()) {
|
|
84
|
+
if (typeof client.stats !== 'function') continue
|
|
85
|
+
let lines
|
|
86
|
+
try {
|
|
87
|
+
lines = formatStats(await client.stats(), opts)
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// No poder leer NO es «no hubo tráfico»: se dice, para que no parezca silencio.
|
|
90
|
+
lines = [`${opts.label ? `[net] ${opts.label}` : '[net]'} could not read stats: ${e?.message || e}`]
|
|
91
|
+
}
|
|
92
|
+
const text = lines.join('\n')
|
|
93
|
+
if (last.get(client) === text) continue
|
|
94
|
+
last.set(client, text)
|
|
95
|
+
for (const line of lines) log(line)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const timer = setInterval(() => { flush() }, everyMs)
|
|
100
|
+
// El log no puede ser lo que mantiene vivo un proceso que ya terminó.
|
|
101
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
flush,
|
|
105
|
+
stop: async () => { clearInterval(timer); await flush() }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/vault@0.
|
|
1
|
+
Copia vendorizada de @dotrino/vault@0.82.1 (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
|
|