@dotrino/identity 0.111.0 → 0.113.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/vault/acta.js +15 -5
- package/vault/core.js +8 -2
- package/vault/remote.js +3 -1
- package/vault/vendor/proxy-client/VERSION.txt +1 -1
- package/vault/vendor/proxy-client/client.js +37 -5
- package/vault/vendor/proxy-client/index.js +1 -1
- package/vault/vendor/proxy-client/stats-log.js +61 -0
- package/vault/vendor/proxy-client/webrtc.js +50 -1
- package/vault/vendor/vault/VERSION.txt +1 -1
- package/vault/vendor/vault/index.js +4 -1
package/package.json
CHANGED
package/vault/acta.js
CHANGED
|
@@ -328,8 +328,9 @@ export async function verifySealerLink (link) {
|
|
|
328
328
|
*
|
|
329
329
|
* Es la hermana de `verifySealerChain`, que hace lo mismo con actas enteras para quien las
|
|
330
330
|
* tiene. Lo que comprueba, y por qué basta:
|
|
331
|
-
* 1. el primero
|
|
332
|
-
* que
|
|
331
|
+
* 1. el primero está firmado por `profileId` — el ancla, que no se puede fabricar sin la
|
|
332
|
+
* llave que da nombre al perfil. Normalmente es el génesis (`seq 1`, sin `prev`), pero
|
|
333
|
+
* no tiene por qué: una cuenta que perdió su génesis ancla en un eslabón posterior;
|
|
333
334
|
* 2. cada siguiente lo firmó alguien a quien el ANTERIOR autorizaba;
|
|
334
335
|
* 3. y apunta al anterior por `seq` + hash, así que no se cuela uno de otra rama.
|
|
335
336
|
*
|
|
@@ -339,7 +340,9 @@ export async function verifySealerLink (link) {
|
|
|
339
340
|
export async function verifySealerLinkChain (chain, { expectedProfileId = null } = {}) {
|
|
340
341
|
if (!Array.isArray(chain) || !chain.length) return { ok: false, reason: 'cadena-vacia' }
|
|
341
342
|
const [raiz] = chain
|
|
342
|
-
|
|
343
|
+
// La misma regla que en `verifySealerChain`: la raíz la ancla la llave del perfil, no el
|
|
344
|
+
// número 1 (una cuenta puede haber perdido su génesis; ver allí).
|
|
345
|
+
if (!Number.isInteger(raiz?.seq) || raiz.seq < 1) return { ok: false, reason: 'no-empieza-en-genesis' }
|
|
343
346
|
if (raiz.by !== raiz.profileId) return { ok: false, reason: 'genesis-no-autofirmado' }
|
|
344
347
|
if (expectedProfileId != null && raiz.profileId !== expectedProfileId) return { ok: false, reason: 'otro-perfil' }
|
|
345
348
|
const vr = await verifySealerLink(raiz)
|
|
@@ -637,8 +640,15 @@ export async function verifySealerChain (chain, { expectedProfileId = null } = {
|
|
|
637
640
|
if (!Array.isArray(chain) || !chain.length) return { ok: false, reason: 'cadena-vacia' }
|
|
638
641
|
const [raiz] = chain
|
|
639
642
|
|
|
640
|
-
|
|
641
|
-
//
|
|
643
|
+
// EL ANCLA ES LA LLAVE DEL PERFIL, NO EL NÚMERO 1 (dueño, 2026-10-14). La raíz vale si
|
|
644
|
+
// está firmada por la llave que da nombre al perfil, sea el génesis o un acta posterior.
|
|
645
|
+
// Antes se exigía `seq 1`, y eso dejaba sin cadena a toda cuenta anterior a las actas v5:
|
|
646
|
+
// su génesis no llevaba `sealerChanged`, la ventana de retención lo podó, y no hay de
|
|
647
|
+
// dónde recuperarlo. Exigir el génesis no protegía de nada: quien tenga la llave del
|
|
648
|
+
// perfil fabrica un génesis alterno igual que un acta posterior, y contra eso la capa es
|
|
649
|
+
// el registro público. Lo que sí sostiene la cadena —que cada eslabón lo firmó alguien a
|
|
650
|
+
// quien el anterior autorizaba— sigue intacto.
|
|
651
|
+
if (!Number.isInteger(raiz.seq) || raiz.seq < 1) return { ok: false, reason: 'no-empieza-en-genesis' }
|
|
642
652
|
if (raiz.sealedBy !== raiz.profileId) return { ok: false, reason: 'genesis-no-autofirmado' }
|
|
643
653
|
if (expectedProfileId != null && raiz.profileId !== expectedProfileId) return { ok: false, reason: 'otro-perfil' }
|
|
644
654
|
const vr = await verifyActa({ acta: raiz })
|
package/vault/core.js
CHANGED
|
@@ -873,7 +873,9 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
873
873
|
h.sort((a, b) => a.seq - b.seq)
|
|
874
874
|
const ventana = h.slice(-ACTA_WINDOW)
|
|
875
875
|
const enVentana = new Set(ventana.map((a) => a.seq))
|
|
876
|
-
|
|
876
|
+
// El génesis cuenta siempre como eslabón: las actas anteriores a v5 no llevaban la
|
|
877
|
+
// marca, y por eso la poda se llevó el génesis de las cuentas viejas.
|
|
878
|
+
const eslabones = h.filter((a) => (a.sealerChanged || a.seq === 1) && !enVentana.has(a.seq))
|
|
877
879
|
kv.setItem(ACTA_HISTORY_STORAGE, JSON.stringify([...eslabones, ...ventana].sort((a, b) => a.seq - b.seq)))
|
|
878
880
|
}
|
|
879
881
|
|
|
@@ -888,7 +890,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
888
890
|
// La vigente cuenta como parte del material aunque todavía no esté en la historia
|
|
889
891
|
// (ahí entra al dejar de serlo). Sin esto, una cuenta recién creada devolvía [].
|
|
890
892
|
const porSeq = new Map([...loadHistory(), cur].map((a) => [a.seq, a]))
|
|
891
|
-
const eslabones = [...porSeq.values()].filter((a) => a.sealerChanged).sort((a, b) => a.seq - b.seq)
|
|
893
|
+
const eslabones = [...porSeq.values()].filter((a) => a.sealerChanged || a.seq === 1).sort((a, b) => a.seq - b.seq)
|
|
892
894
|
// Y la actual va al final si ella misma no es un eslabón: el verificador necesita ver
|
|
893
895
|
// la cabeza para saber en qué `seq` está y quién la selló.
|
|
894
896
|
return eslabones.length && eslabones[eslabones.length - 1].seq === cur.seq ? eslabones : [...eslabones, cur]
|
|
@@ -2506,6 +2508,10 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
2506
2508
|
* Una cuenta de una sola bóveda devuelve SIEMPRE eso y nada más, y no puede quedarse
|
|
2507
2509
|
* obsoleta: su conjunto de selladores no puede cambiar, porque el único no puede
|
|
2508
2510
|
* quitarse a sí mismo y no hay otro que se lo quite.
|
|
2511
|
+
*
|
|
2512
|
+
* SIN GÉNESIS, la cadena es `[acta actual]` y vale igual mientras la actual esté firmada
|
|
2513
|
+
* por la llave del perfil: la raíz la ancla esa llave, no el número 1 (`verifySealerChain`).
|
|
2514
|
+
* Pasa en las cuentas anteriores a v5, cuyo génesis se podó sin la marca de eslabón.
|
|
2509
2515
|
*/
|
|
2510
2516
|
async sealerChain () { return sealerChain() },
|
|
2511
2517
|
|
package/vault/remote.js
CHANGED
|
@@ -374,7 +374,9 @@ export async function requestStore ({ master, proxy, device, cert, method, args,
|
|
|
374
374
|
export async function requestDevices ({ master, proxy, device, cert, sinceSeq, onRevoked } = {}) {
|
|
375
375
|
const data = typeof sinceSeq === 'number' ? { op: 'devices', sinceSeq } : { op: 'devices' }
|
|
376
376
|
const res = await vaultRpc({ master, proxy, device, cert, onRevoked, sendType: 'vault.devices', okType: 'vault.devices.result', data })
|
|
377
|
-
|
|
377
|
+
// `sealerChain`: [génesis, …cambios de sellador…, actual], lo que una firma de este aparato
|
|
378
|
+
// tiene que llevar para que `verifySignedFor` la ate a la cuenta (vaultd ≥ 0.154.0).
|
|
379
|
+
return { devices: res.devices || [], revoked: res.revoked || [], acta: res.acta || null, chain: res.chain || null, sealerChain: res.sealerChain || null }
|
|
378
380
|
}
|
|
379
381
|
|
|
380
382
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/proxy-client@0.
|
|
1
|
+
Copia vendorizada de @dotrino/proxy-client@0.32.0 (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.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildSignedChannel, getPublicKeyJwk, signData, samePubkey } from './signature.js'
|
|
2
2
|
import { seal, open, isSealed } from './sealing.js'
|
|
3
3
|
import { buildEncPubStatement, readEncPubStatement, isEncPub } from './encpub.js'
|
|
4
|
-
import { WebRTCManager, RTC_TAG, DEFAULT_ICE_SERVERS, loadNodePeerConnection, resolvePeerConnection } from './webrtc.js'
|
|
4
|
+
import { WebRTCManager, RTC_TAG, DEFAULT_ICE_SERVERS, loadNodePeerConnection, resolvePeerConnection, pinTurnToIPv4 } from './webrtc.js'
|
|
5
5
|
import { TrafficStats, utf8Length, registerTransport, unregisterTransport } from './stats.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -1002,9 +1002,35 @@ export class WebSocketProxyClient {
|
|
|
1002
1002
|
clearTimeout(this._turnTimer)
|
|
1003
1003
|
this._turnTimer = null
|
|
1004
1004
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1005
|
+
let res
|
|
1006
|
+
try {
|
|
1007
|
+
res = await this.getTurnCredentials({ publicKey, sign })
|
|
1008
|
+
} catch (e) {
|
|
1009
|
+
if (e?.status !== 429) throw e
|
|
1010
|
+
// 429: EL PROXIO DICE «AHORA NO» (tope diario de tráfico, cuota por hora, o no puede
|
|
1011
|
+
// leer el consumo). No es un fallo: se sigue por el proxio, que es el escalón que
|
|
1012
|
+
// siempre funciona. Se quita el relevo que hubiera (sus credenciales ya no se
|
|
1013
|
+
// renuevan), se dice por un evento y en `stats()`, y se vuelve a pedir cuando el
|
|
1014
|
+
// proxio dice que se puede — si no, un aparato que pasó por un día con el tope
|
|
1015
|
+
// alcanzado se quedaría sin relevo hasta su siguiente reconexión.
|
|
1016
|
+
this._rtc.setIceServers(DEFAULT_ICE_SERVERS)
|
|
1017
|
+
const retryAfterMs = Math.max(60000, Number(e.retryAfterMs) || 0)
|
|
1018
|
+
this._turn = { active: false, code: e.code || null, retryAt: Date.now() + retryAfterMs }
|
|
1019
|
+
this._emit('turn_unavailable', { code: e.code || null, retryAfterMs })
|
|
1020
|
+
this._turnTimer = setTimeout(() => {
|
|
1021
|
+
this._turnTimer = null
|
|
1022
|
+
this.enableTurn({ publicKey, sign }).catch(() => {})
|
|
1023
|
+
}, retryAfterMs)
|
|
1024
|
+
return false
|
|
1025
|
+
}
|
|
1026
|
+
if (!res.enabled || !res.iceServers) {
|
|
1027
|
+
this._turn = { active: false, code: 'turn-disabled', retryAt: null }
|
|
1028
|
+
return false
|
|
1029
|
+
}
|
|
1030
|
+
this._turn = { active: true, code: null, expiresAt: res.expiresAt || null }
|
|
1031
|
+
// En Node el relevo va con su dirección IPv4 ya escrita (ver `pinTurnToIPv4`).
|
|
1032
|
+
const turnServers = await pinTurnToIPv4(res.iceServers)
|
|
1033
|
+
this._rtc.setIceServers([...turnServers, ...DEFAULT_ICE_SERVERS])
|
|
1008
1034
|
// Renovar con margen antes de expirar (mínimo 30 s entre renovaciones)
|
|
1009
1035
|
const refreshIn = Math.max(30000, (res.expiresAt || 0) - Date.now() - 60000)
|
|
1010
1036
|
this._turnTimer = setTimeout(() => {
|
|
@@ -1523,6 +1549,8 @@ export class WebSocketProxyClient {
|
|
|
1523
1549
|
publickey: this.myPublickey,
|
|
1524
1550
|
connected: this._connected,
|
|
1525
1551
|
webrtc: !!this._rtc,
|
|
1552
|
+
// El relevo: null si nunca se pidió; si no, activo o por qué no (`code`, `retryAt`).
|
|
1553
|
+
turn: this._turn ? { ...this._turn } : null,
|
|
1526
1554
|
since: t.since,
|
|
1527
1555
|
proxy: { ...t.proxy },
|
|
1528
1556
|
peers
|
|
@@ -1566,7 +1594,11 @@ export class WebSocketProxyClient {
|
|
|
1566
1594
|
this._pending.delete(id)
|
|
1567
1595
|
// Con su `code`: quien llama distingue los casos por él, no por la frase (que el proxio
|
|
1568
1596
|
// puede cambiar o traducir). Antes se perdía aquí y solo quedaba comparar el texto.
|
|
1569
|
-
|
|
1597
|
+
const e = errorCon(data.error || 'Server error', data.code || null)
|
|
1598
|
+
// «Ahora no» (429) y cuánto esperar, cuando el proxio lo dice.
|
|
1599
|
+
if (data.status) e.status = data.status
|
|
1600
|
+
if (data.retry_after_ms) e.retryAfterMs = data.retry_after_ms
|
|
1601
|
+
entry.reject(e)
|
|
1570
1602
|
}
|
|
1571
1603
|
|
|
1572
1604
|
_emit (event, ...args) {
|
|
@@ -11,7 +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
|
+
export { logStats, formatStats, netReport, splitPeers } from './stats-log.js'
|
|
15
15
|
|
|
16
16
|
import { WebSocketProxyClient } from './client.js'
|
|
17
17
|
|
|
@@ -54,6 +54,7 @@ export function formatStats (s, opts = {}) {
|
|
|
54
54
|
` ws.in=${s.proxy.bytesIn} ws.out=${s.proxy.bytesOut}` +
|
|
55
55
|
` ${pairs('in', sum(s.peers, 'bytesIn'))} ${pairs('out', sum(s.peers, 'bytesOut'))}` +
|
|
56
56
|
` peers=${talking.length} routes=${routeList}` +
|
|
57
|
+
(s.turn ? ` turn=${s.turn.active ? 'on' : `off(${s.turn.code || '?'})`}` : '') +
|
|
57
58
|
(silent.length ? ` unanswered=${silent.length} unanswered.out=${silent.reduce((n, p) => n + PATHS.reduce((m, k) => m + p.bytesOut[k], 0), 0)}` : '') +
|
|
58
59
|
(unknown.length ? ` unknown=${unknown.length} unknown.in=${unknown.reduce((n, p) => n + PATHS.reduce((m, k) => m + p.bytesIn[k], 0), 0)}` : '')
|
|
59
60
|
]
|
|
@@ -105,3 +106,63 @@ export function logStats (opts = {}) {
|
|
|
105
106
|
stop: async () => { clearInterval(timer); await flush() }
|
|
106
107
|
}
|
|
107
108
|
}
|
|
109
|
+
|
|
110
|
+
/** Bytes para el informe de texto: `B`, `KB` con un decimal, `MB` con dos. */
|
|
111
|
+
const rBytes = (n) => (n < 1024 ? `${n} B` : n < 1024 * 1024 ? `${(n / 1024).toFixed(1)} KB` : `${(n / 1024 / 1024).toFixed(2)} MB`)
|
|
112
|
+
const rPaths = (o) => {
|
|
113
|
+
const parts = PATHS.filter((k) => (o?.[k] || 0) > 0).map((k) => `${k} ${rBytes(o[k])}`)
|
|
114
|
+
return parts.length ? parts.join(', ') : '0 B'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Los que contestaron algo (`talking`) y los que solo recibieron (`silent`, resumidos): a un
|
|
119
|
+
* aparato apagado al que se le mandó un ping no se le llama conexión. `unknown`: quien nos
|
|
120
|
+
* escribió, nunca recibió respuesta y no sabemos su llave. Tampoco es una conexión.
|
|
121
|
+
*
|
|
122
|
+
* @param {Array<object>} peers `TransportStats.peers`
|
|
123
|
+
*/
|
|
124
|
+
export function splitPeers (peers) {
|
|
125
|
+
const total = (o) => PATHS.reduce((n, k) => n + (o?.[k] || 0), 0)
|
|
126
|
+
const talking = []
|
|
127
|
+
const silent = { count: 0, bytesOut: 0, msgsOut: 0 }
|
|
128
|
+
const unknown = { count: 0, bytesIn: 0, msgsIn: 0 }
|
|
129
|
+
for (const p of peers) {
|
|
130
|
+
if (!((p.msgsIn || 0) > 0)) { silent.count++; silent.bytesOut += total(p.bytesOut); silent.msgsOut += p.msgsOut || 0 } else if (!p.pubkey && !((p.msgsOut || 0) > 0)) { unknown.count++; unknown.bytesIn += total(p.bytesIn); unknown.msgsIn += p.msgsIn || 0 } else talking.push(p)
|
|
131
|
+
}
|
|
132
|
+
return { talking, silent, unknown }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* EL INFORME DE RED COMO TEXTO, para que lo lea una persona: las mismas líneas que el modal de
|
|
137
|
+
* red del topbar y que las apps nativas (`NetworkStats.report`). A diferencia de `formatStats`
|
|
138
|
+
* (que va a un log en disco y por eso calla la llave), este se enseña en pantalla a quien lo
|
|
139
|
+
* pidió, y dice con quién es cada conexión.
|
|
140
|
+
*
|
|
141
|
+
* @param {Array<object>} list lo que devolvió `client.stats()` de cada cliente (o `{ url, error }`)
|
|
142
|
+
* @param {{ label?: (pubkey: string) => string | Promise<string>, now?: Date }} [opts]
|
|
143
|
+
* `label`: cómo se nombra un aparato a partir de su llave (su id `AB12-CD34`, su nombre).
|
|
144
|
+
* Sin él, o si falla, sale `?`: nunca un trozo de la llave.
|
|
145
|
+
* @returns {Promise<string>}
|
|
146
|
+
*/
|
|
147
|
+
export async function netReport (list, opts = {}) {
|
|
148
|
+
const now = opts.now || new Date()
|
|
149
|
+
let out = `Dotrino network stats · ${now.toISOString().replace(/\.\d{3}Z$/, 'Z')}\n`
|
|
150
|
+
if (!list.length) out += '(no transports)\n'
|
|
151
|
+
for (const s of list) {
|
|
152
|
+
if (s.error) { out += `Proxy ${s.url} | error: ${s.error}\n`; continue }
|
|
153
|
+
const hhmm = new Date(s.since).toTimeString().slice(0, 8)
|
|
154
|
+
out += `Proxy ${s.url}${s.app ? ` | app=${s.app}` : ''}${s.node ? ` | node=${s.node}` : ''} | ${s.connected ? 'connected' : 'disconnected'} | since ${hhmm}\n`
|
|
155
|
+
out += ` proxy total: in ${rBytes(s.proxy.bytesIn)} / out ${rBytes(s.proxy.bytesOut)} (frames ${s.proxy.framesIn ?? '?'}/${s.proxy.framesOut ?? '?'})\n`
|
|
156
|
+
const { talking, silent, unknown } = splitPeers(s.peers)
|
|
157
|
+
out += ` peers: ${talking.length}\n`
|
|
158
|
+
for (const p of talking) {
|
|
159
|
+
let who = '?'
|
|
160
|
+
if (p.pubkey && opts.label) { try { who = (await opts.label(p.pubkey)) || '?' } catch (_) { who = '?' } }
|
|
161
|
+
const tok = p.token ? ` (token ${p.token})` : ''
|
|
162
|
+
out += ` - ${who}${tok} | route=${p.route} | in: ${rPaths(p.bytesIn)} | out: ${rPaths(p.bytesOut)} | ${(p.msgsIn || 0) + (p.msgsOut || 0)} msgs\n`
|
|
163
|
+
}
|
|
164
|
+
if (silent.count) out += ` no answer: ${silent.count} (out ${rBytes(silent.bytesOut)}, ${silent.msgsOut} msgs)\n`
|
|
165
|
+
if (unknown.count) out += ` unknown senders: ${unknown.count} (in ${rBytes(unknown.bytesIn)}, ${unknown.msgsIn} msgs)\n`
|
|
166
|
+
}
|
|
167
|
+
return out
|
|
168
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `send()` payloads tagged with `_rtc`.
|
|
7
7
|
* - Once the DataChannel is open, payloads from `client.send(token, ...)`
|
|
8
8
|
* travel P2P; before that (or on failure) they fall back to the proxy.
|
|
9
|
-
* - STUN
|
|
9
|
+
* - STUN by default; `client.enableTurn()` adds the proxy's temporary TURN servers.
|
|
10
10
|
*
|
|
11
11
|
* Glare resolution: the peer with the lexicographically smaller token is
|
|
12
12
|
* the "polite" one (rolls back on collision).
|
|
@@ -76,6 +76,55 @@ export async function loadNodePeerConnection () {
|
|
|
76
76
|
return _PC
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* EL RELEVO, FIJADO A IPv4 — solo fuera del navegador.
|
|
81
|
+
*
|
|
82
|
+
* La implementación de Node (`@dotrino/webrtc`, o sea werift) abre el socket UDP hacia el
|
|
83
|
+
* relevo en IPv4 y nada más, pero resuelve el nombre sin pedir familia. En una máquina con
|
|
84
|
+
* las dos, el nombre sale en IPv6: el pedido se manda a una dirección que ese socket no
|
|
85
|
+
* alcanza, se reintenta siete veces (6,4 s medidos) y solo entonces se cae a TCP. Y como la
|
|
86
|
+
* oferta espera a que termine de reunir candidatos, CADA negociación arrancaba 6,4 s tarde
|
|
87
|
+
* por punta — una sesión más corta que eso no salía nunca del proxio.
|
|
88
|
+
*
|
|
89
|
+
* Con la dirección ya escrita no hay nada que resolver: 30 ms, y el relevo va por UDP.
|
|
90
|
+
*
|
|
91
|
+
* Solo las `turn:`. Una `turns:` necesita el nombre para comprobar el certificado, y a un
|
|
92
|
+
* `stun:` no le pasa esto. Se envuelve desde aquí y no se toca la librería, que es una poda
|
|
93
|
+
* sin cambios de lógica (README de `dotrino-webrtc`).
|
|
94
|
+
*
|
|
95
|
+
* Si el nombre no tiene dirección IPv4, se deja como vino: esa máquina no tiene por dónde
|
|
96
|
+
* llegar en UDP, y con el nombre la librería todavía llega por TCP.
|
|
97
|
+
*
|
|
98
|
+
* @param {any[]} iceServers
|
|
99
|
+
* @param {{ native?: boolean, lookup?: (host: string) => Promise<string> }} [opts]
|
|
100
|
+
* @returns {Promise<any[]>}
|
|
101
|
+
*/
|
|
102
|
+
export async function pinTurnToIPv4 (iceServers, opts = {}) {
|
|
103
|
+
const native = opts.native ?? (typeof globalThis.RTCPeerConnection === 'function')
|
|
104
|
+
if (native || !Array.isArray(iceServers)) return iceServers
|
|
105
|
+
let lookup = opts.lookup
|
|
106
|
+
if (!lookup) {
|
|
107
|
+
// El nombre va en una variable para que ningún empaquetador lo siga: esto lo importan
|
|
108
|
+
// las apps de navegador, donde `node:dns` no existe (y donde no se llega hasta aquí).
|
|
109
|
+
const mod = 'node:dns'
|
|
110
|
+
const dns = await import(mod)
|
|
111
|
+
lookup = async (host) => (await dns.promises.lookup(host, { family: 4 })).address
|
|
112
|
+
}
|
|
113
|
+
const cache = new Map()
|
|
114
|
+
const pin = async (url) => {
|
|
115
|
+
const m = /^(turn:)([^:?[\]]+)((?::\d+)?(?:\?.*)?)$/i.exec(String(url).trim())
|
|
116
|
+
if (!m || /^[\d.]+$/.test(m[2])) return url
|
|
117
|
+
if (!cache.has(m[2])) cache.set(m[2], lookup(m[2]).catch(() => null))
|
|
118
|
+
const ip = await cache.get(m[2])
|
|
119
|
+
return ip ? m[1] + ip + m[3] : url
|
|
120
|
+
}
|
|
121
|
+
return Promise.all(iceServers.map(async (s) => {
|
|
122
|
+
if (!s || s.urls == null) return s
|
|
123
|
+
const urls = Array.isArray(s.urls) ? await Promise.all(s.urls.map(pin)) : await pin(s.urls)
|
|
124
|
+
return { ...s, urls }
|
|
125
|
+
}))
|
|
126
|
+
}
|
|
127
|
+
|
|
79
128
|
export class WebRTCManager {
|
|
80
129
|
/**
|
|
81
130
|
* @param {object} opts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/vault@0.
|
|
1
|
+
Copia vendorizada de @dotrino/vault@0.84.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
|
|
@@ -217,7 +217,10 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
217
217
|
deviceId: x.sub ? await deviceIdOf(x.sub) : null, sub: x.sub || null,
|
|
218
218
|
label: x.label || '', scope: x.scope, seq: x.seq, nonce: x.nonce
|
|
219
219
|
})))
|
|
220
|
-
|
|
220
|
+
// La cadena de selladores va con la lista (como en el daemon): es lo que un aparato de
|
|
221
|
+
// Node necesita para firmar a nombre de la cuenta de forma que un extraño lo compruebe.
|
|
222
|
+
const sealerChain = await identity.sealerChain?.().catch(() => null) || null
|
|
223
|
+
send(from, { type: MSG.DEVICES_RESULT, devices, revoked: (revoked || []).map((r) => r.nonce || r), sealerChain })
|
|
221
224
|
// ¿el que consulta es una máquina revocada que reapareció? → re-emite el REVOKED firmado.
|
|
222
225
|
// Se busca en `revokedCerts`: desde que `issued` solo trae lo vigente, los retirados
|
|
223
226
|
// ya no están ahí y este aviso dejaba de dispararse (se caía en silencio).
|