@dotrino/identity 0.115.0 → 0.117.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 +4 -1
- package/src/node.js +4 -180
- package/vault/content.js +28 -1
- package/vault/core.js +66 -7
- package/vault/facade.js +200 -0
- package/vault/index.html +10 -0
- package/vault/remote.js +45 -5
- package/vault/vault.js +29 -58
- package/vault/vendor/compat/VERSION.txt +3 -0
- package/vault/vendor/compat/advisory.js +141 -0
- package/vault/vendor/compat/index.js +188 -0
- package/vault/vendor/compat/ranges.js +93 -0
- package/vault/vendor/opaque/VERSION.txt +1 -1
- package/vault/vendor/passmanager/VERSION.txt +3 -0
- package/vault/vendor/passmanager/crypto.js +167 -0
- package/vault/vendor/passmanager/fields.js +183 -0
- package/vault/vendor/passmanager/generate.js +74 -0
- package/vault/vendor/passmanager/import.js +156 -0
- package/vault/vendor/passmanager/index.js +27 -0
- package/vault/vendor/passmanager/match.js +137 -0
- package/vault/vendor/passmanager/model.js +332 -0
- package/vault/vendor/passmanager/pubkey.js +33 -0
- package/vault/vendor/passmanager/sealed/convert.js +72 -0
- package/vault/vendor/passmanager/sealed/device.js +211 -0
- package/vault/vendor/passmanager/sealed/entry.js +209 -0
- package/vault/vendor/passmanager/sealed/index.js +20 -0
- package/vault/vendor/passmanager/sealed/keys.js +120 -0
- package/vault/vendor/passmanager/sealed/recovery.js +136 -0
- package/vault/vendor/passmanager/sealed/store.js +508 -0
- package/vault/vendor/passmanager/session-cache.js +60 -0
- package/vault/vendor/passmanager/totp.js +111 -0
- package/vault/vendor/passmanager/transport/protocol.js +83 -0
- package/vault/vendor/passmanager/transport/proxy.js +112 -0
- package/vault/vendor/passmanager/transport/sealed.js +74 -0
- package/vault/vendor/passmanager/vault/approval.js +63 -0
- package/vault/vendor/passmanager/vault/errors.js +34 -0
- package/vault/vendor/passmanager/vault/guard.js +114 -0
- package/vault/vendor/passmanager/vault/interface.js +34 -0
- package/vault/vendor/passmanager/vault/local.js +213 -0
- package/vault/vendor/passmanager/vault/remote.js +85 -0
- package/vault/vendor/passmanager/vault/responder.js +192 -0
- package/vault/vendor/passmanager/vault/sealed-local.js +199 -0
- package/vault/vendor/passmanager/vault/sealed-responder.js +159 -0
- package/vault/vendor/passmanager/vault/sealed.js +362 -0
- package/vault/vendor/passmanager/webauthn.js +221 -0
- package/vault/vendor/proxy-client/VERSION.txt +1 -1
- package/vault/vendor/proxy-client/client.js +136 -11
- package/vault/vendor/proxy-client/index.js +1 -0
- package/vault/vendor/proxy-client/parts.js +127 -0
- package/vault/vendor/proxy-client/webrtc.js +23 -0
- package/vault/vendor/roadmap/VERSION.txt +3 -0
- package/vault/vendor/roadmap/index.js +108 -0
- package/vault/vendor/roadmap/manifests.js +802 -0
- package/vault/vendor/store/VERSION.txt +3 -0
- package/vault/vendor/store/core.js +309 -0
- package/vault/vendor/vault/VERSION.txt +4 -1
- package/vault/vendor/vault/admin.js +437 -0
- package/vault/vendor/vault/browser.js +167 -0
- package/vault/vendor/vault/core/approvals.js +204 -0
- package/vault/vendor/vault/core/blocked.js +68 -0
- package/vault/vendor/vault/core/browserDisk.js +109 -0
- package/vault/vendor/vault/core/commKey.js +68 -0
- package/vault/vendor/vault/core/disk.js +65 -0
- package/vault/vendor/vault/core/libVersion.js +8 -0
- package/vault/vendor/vault/core/machines.js +66 -0
- package/vault/vendor/vault/core/notices.js +71 -0
- package/vault/vendor/vault/core/notifiable.js +73 -0
- package/vault/vendor/vault/core/passwords.js +125 -0
- package/vault/vendor/vault/core/sealKey.js +77 -0
- package/vault/vendor/vault/core/sealer.js +172 -0
- package/vault/vendor/vault/core/sealers.js +159 -0
- package/vault/vendor/vault/core/secretsStore.js +1326 -0
- package/vault/vendor/vault/core/sha256.js +69 -0
- package/vault/vendor/vault/core/store.js +68 -0
- package/vault/vendor/vault/core/subacta.js +101 -0
- package/vault/vendor/vault/core/threadStore.js +273 -0
- package/vault/vendor/vault/core/transport.js +270 -0
- package/vault/vendor/vault/core/vault.js +4414 -0
- package/vault/vendor/vault/enroll.js +8 -4
- package/vault/vendor/vault/procContext.js +97 -0
- package/vault/vendor/vault/revocation.js +67 -0
- package/vault/vendor/vault/sealed.js +84 -0
|
@@ -3,6 +3,7 @@ import { seal, open, isSealed } from './sealing.js'
|
|
|
3
3
|
import { buildEncPubStatement, readEncPubStatement, isEncPub } from './encpub.js'
|
|
4
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
|
+
import { splitParts, isPart, Reassembler, DIRECT_PART_CHARS, PROXY_PART_CHARS, QUEUE_MAX_CHARS, RELAY_MAX_CHARS } from './parts.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Error con un `code` estable.
|
|
@@ -67,6 +68,12 @@ export class WebSocketProxyClient {
|
|
|
67
68
|
this.autoReconnect = options.autoReconnect !== false
|
|
68
69
|
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5
|
|
69
70
|
this.reconnectDelay = options.reconnectDelay ?? 3000
|
|
71
|
+
// UN SOCKET QUE NO ABRE NI FALLA. El WebSocket de Node (y el del navegador) no tiene
|
|
72
|
+
// tope para el saludo: si el TCP entra y nadie contesta el upgrade, se queda en
|
|
73
|
+
// CONNECTING sin disparar `open`, `error` ni `close`. Como el siguiente reintento solo
|
|
74
|
+
// se programa desde esos eventos, la cadena de reconexión se paraba ahí, en silencio.
|
|
75
|
+
// Se abandona el intento pasado este tiempo y cuenta como un fallo más.
|
|
76
|
+
this.connectTimeout = options.connectTimeout ?? 15000
|
|
70
77
|
this.enableWebRTC = options.enableWebRTC !== false
|
|
71
78
|
this.iceServers = options.iceServers || null
|
|
72
79
|
|
|
@@ -180,7 +187,12 @@ export class WebSocketProxyClient {
|
|
|
180
187
|
this._handlers = new Map()
|
|
181
188
|
this._pending = new Map() // messageId -> { resolve, reject, timer }
|
|
182
189
|
this._nextId = 1
|
|
183
|
-
this._tokenWatch = new Map() // messageId -> { token, sobre, peerPubkey, timer }
|
|
190
|
+
this._tokenWatch = new Map() // messageId -> { token, sobre, peerPubkey, timer, queueFallback }
|
|
191
|
+
// Mensajes grandes que llegan en partes (parts.js). Uno que se queda a medias se DICE:
|
|
192
|
+
// con él se pierde el mensaje entero, y eso no puede pasar callado.
|
|
193
|
+
this._parts = new Reassembler({
|
|
194
|
+
onExpire: (info) => this._emit('error', { type: 'parts_incomplete', code: 'parts-incomplete', ...info })
|
|
195
|
+
})
|
|
184
196
|
this._traffic = new TrafficStats()
|
|
185
197
|
|
|
186
198
|
this._rtc = this.enableWebRTC ? new WebRTCManager({
|
|
@@ -225,6 +237,8 @@ export class WebSocketProxyClient {
|
|
|
225
237
|
connect () {
|
|
226
238
|
return new Promise((resolve, reject) => {
|
|
227
239
|
if (this._connected) return resolve(this.token)
|
|
240
|
+
// Un reintento que estaba esperando su turno sobra: este `connect()` ya abre uno.
|
|
241
|
+
if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null }
|
|
228
242
|
this._connectResolve = resolve
|
|
229
243
|
this._connectReject = reject
|
|
230
244
|
this._open()
|
|
@@ -493,10 +507,19 @@ export class WebSocketProxyClient {
|
|
|
493
507
|
*/
|
|
494
508
|
async sendSealed (toPubkeys, payload, { peerEncPub, ...opts } = {}) {
|
|
495
509
|
const list = Array.isArray(toPubkeys) ? toPubkeys : [toPubkeys]
|
|
510
|
+
// Por pubkey el mensaje puede quedarse ESPERANDO en el proxio, y ahí se deja como mucho
|
|
511
|
+
// 1 MB (parts.js). Lo que pasa de eso no se manda: se dice, y la app decide.
|
|
512
|
+
const json = JSON.stringify(payload)
|
|
513
|
+
if (json.length > QUEUE_MAX_CHARS) {
|
|
514
|
+
throw errorCon(`sendSealed: ${json.length} chars is more than can wait in the proxy (${QUEUE_MAX_CHARS}); send it by token while both ends are connected`, 'too-big-for-queue')
|
|
515
|
+
}
|
|
516
|
+
const pieces = json.length > PROXY_PART_CHARS ? splitParts(json, PROXY_PART_CHARS) : [payload]
|
|
496
517
|
// Con la llave puesta a mano se respeta tal cual: quien la pasa está diciendo que ya
|
|
497
518
|
// sabe de quién es (se emparejaron), y un sobre vale para todos los destinatarios.
|
|
498
519
|
if (peerEncPub) {
|
|
499
|
-
|
|
520
|
+
for (const piece of pieces) {
|
|
521
|
+
this._sendByPubkeyRaw(list, await this._seal(piece, peerEncPub, list.length === 1 ? list[0] : undefined), opts)
|
|
522
|
+
}
|
|
500
523
|
return
|
|
501
524
|
}
|
|
502
525
|
// Sin ella, se averigua. UNA ENVOLTURA POR DESTINATARIO: cada uno tiene su llave, así
|
|
@@ -507,7 +530,7 @@ export class WebSocketProxyClient {
|
|
|
507
530
|
// sin forma de saber cuál mitad.
|
|
508
531
|
const llaves = await Promise.all(list.map(async (pk) => [pk, await this.encPubOf(pk)]))
|
|
509
532
|
for (const [pk, encPub] of llaves) {
|
|
510
|
-
this._sendByPubkeyRaw([pk], await this._seal(
|
|
533
|
+
for (const piece of pieces) this._sendByPubkeyRaw([pk], await this._seal(piece, encPub, pk), opts)
|
|
511
534
|
}
|
|
512
535
|
}
|
|
513
536
|
|
|
@@ -539,10 +562,70 @@ export class WebSocketProxyClient {
|
|
|
539
562
|
peerEncPub = await this.encPubOf(peerPubkey)
|
|
540
563
|
}
|
|
541
564
|
if (!peerPubkey && tokens.length === 1) peerPubkey = this.pubkeyOfToken(tokens[0]) || undefined
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
//
|
|
545
|
-
|
|
565
|
+
// Por token los dos están conectados y NO HAY TOPE (parts.js): lo que no cabe en un
|
|
566
|
+
// mensaje del camino que toca sale en partes. El tamaño de parte lo decide el camino
|
|
567
|
+
// de ahora: el canal directo admite mucho menos por mensaje que el proxio.
|
|
568
|
+
const json = JSON.stringify(payload)
|
|
569
|
+
const direct = tokens.length === 1 && !!this._rtc?.isOpen(tokens[0])
|
|
570
|
+
const size = direct ? DIRECT_PART_CHARS : PROXY_PART_CHARS
|
|
571
|
+
if (json.length <= size) {
|
|
572
|
+
const sobre = await this._seal(payload, peerEncPub, peerPubkey)
|
|
573
|
+
if (tokens.length === 1 && peerPubkey) { this._sendToTokenOrQueue(tokens[0], sobre, peerPubkey); return }
|
|
574
|
+
// Por `send`, no por `_sendRaw`: así sigue prefiriendo el canal directo si lo hay.
|
|
575
|
+
this.send(tokens, sobre)
|
|
576
|
+
return
|
|
577
|
+
}
|
|
578
|
+
// Por el proxio en vivo hay tope (parts.js): lo que pasa de él solo sale por canal
|
|
579
|
+
// directo. Se pide el canal para que el siguiente intento lo encuentre abierto.
|
|
580
|
+
if (!direct && json.length > RELAY_MAX_CHARS) {
|
|
581
|
+
this._upgradeDirect(tokens)
|
|
582
|
+
throw errorCon(`sendSealedTo: ${json.length} chars is more than goes through the proxy live (${RELAY_MAX_CHARS}); it needs a direct channel`, 'too-big-for-relay')
|
|
583
|
+
}
|
|
584
|
+
// Si el token muere a mitad, lo que quede se iría a la cola por pubkey; solo se permite
|
|
585
|
+
// si el mensaje entero cabe ahí. Si no, se dice (`too-big-for-queue`), no se deja.
|
|
586
|
+
const queueFallback = json.length <= QUEUE_MAX_CHARS
|
|
587
|
+
// Y si el canal directo se cae a mitad, el resto va por el proxio en vivo solo si el
|
|
588
|
+
// mensaje entero cabe por ahí; si no, se para y se dice (`direct-lost`).
|
|
589
|
+
const relayFallback = json.length <= RELAY_MAX_CHARS
|
|
590
|
+
for (const piece of splitParts(json, size)) {
|
|
591
|
+
const sobre = await this._seal(piece, peerEncPub, peerPubkey)
|
|
592
|
+
if (tokens.length === 1 && peerPubkey) await this._sendPiece(tokens[0], sobre, peerPubkey, queueFallback, relayFallback)
|
|
593
|
+
else this.send(tokens, sobre)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* POR DÓNDE LE LLEGA AHORA un mensaje a este token: `socket` (la otra punta en esta
|
|
599
|
+
* máquina), `direct` (WebRTC sin nadie en medio), `turn` (WebRTC por el relevo, que no puede
|
|
600
|
+
* leer lo que pasa), `proxy` (en vivo por el proxio) u `offline` (no hay conexión: por
|
|
601
|
+
* pubkey quedaría esperando en el proxio). Cada camino tiene su tope (parts.js), y para eso
|
|
602
|
+
* existe esto: que la app lo enseñe antes de que el usuario intente algo que no cabe.
|
|
603
|
+
* `webrtc` es un canal directo abierto del que todavía no se sabe si va por TURN.
|
|
604
|
+
* @param {string|null} token
|
|
605
|
+
* @returns {Promise<'socket'|'direct'|'turn'|'webrtc'|'proxy'|'offline'>}
|
|
606
|
+
*/
|
|
607
|
+
async routeTo (token) {
|
|
608
|
+
if (!this._connected) return 'offline'
|
|
609
|
+
if (!token) return 'offline'
|
|
610
|
+
if (this._rtc?.isOpen(token)) {
|
|
611
|
+
const peer = this._rtc.peers.get(token)
|
|
612
|
+
try { return (await this._rtc.probeRoute(peer)) || 'webrtc' } catch (_) { return 'webrtc' }
|
|
613
|
+
}
|
|
614
|
+
return 'proxy'
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Una parte por token: por el canal directo esperando a que vacíe su búfer (sin eso, un
|
|
619
|
+
* archivo grande lo desborda y el navegador cierra el canal), o por el proxio en vivo con
|
|
620
|
+
* una pausa corta entre partes, para no chocar con su límite de mensajes por segundo.
|
|
621
|
+
*/
|
|
622
|
+
async _sendPiece (token, sobre, peerPubkey, queueFallback, relayFallback = true) {
|
|
623
|
+
if (this._rtc && await this._rtc.sendDirect(token, JSON.stringify(sobre))) return
|
|
624
|
+
if (!relayFallback) {
|
|
625
|
+
throw errorCon('the direct channel closed in the middle of a message too big for the proxy', 'direct-lost')
|
|
626
|
+
}
|
|
627
|
+
this._sendToTokenOrQueue(token, sobre, peerPubkey, { queueFallback, skipDirect: true })
|
|
628
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
546
629
|
}
|
|
547
630
|
|
|
548
631
|
/**
|
|
@@ -577,16 +660,16 @@ export class WebSocketProxyClient {
|
|
|
577
660
|
* `token_gone` para que la app deje de usar ese token. No bloquea a nadie: se manda ya y
|
|
578
661
|
* el arreglo, si hace falta, llega detrás.
|
|
579
662
|
*/
|
|
580
|
-
_sendToTokenOrQueue (token, sobre, peerPubkey) {
|
|
663
|
+
_sendToTokenOrQueue (token, sobre, peerPubkey, { queueFallback = true, skipDirect = false } = {}) {
|
|
581
664
|
if (this.requireSealed && !this._isSealed(sobre)) {
|
|
582
665
|
throw errorCon('requireSealed: refusing to send a directed message in the clear — use sendSealedTo()', 'unsealed')
|
|
583
666
|
}
|
|
584
667
|
const messageStr = JSON.stringify(sobre)
|
|
585
|
-
if (this._rtc && this._rtc.trySend(token, messageStr)) return
|
|
668
|
+
if (!skipDirect && this._rtc && this._rtc.trySend(token, messageStr)) return
|
|
586
669
|
const id = `msg_${this._nextId++}`
|
|
587
670
|
const timer = setTimeout(() => this._tokenWatch.delete(id), 15000)
|
|
588
671
|
timer.unref?.()
|
|
589
|
-
this._tokenWatch.set(id, { token, sobre, peerPubkey, timer })
|
|
672
|
+
this._tokenWatch.set(id, { token, sobre, peerPubkey, timer, queueFallback })
|
|
590
673
|
try {
|
|
591
674
|
this._sendRaw({ to: [token], message: messageStr, id })
|
|
592
675
|
} catch (e) {
|
|
@@ -605,6 +688,11 @@ export class WebSocketProxyClient {
|
|
|
605
688
|
this._tokenWatch.delete(data.id)
|
|
606
689
|
if (!Array.isArray(data.failed) || !data.failed.includes(w.token)) return
|
|
607
690
|
this._emit('token_gone', w.token, w.peerPubkey)
|
|
691
|
+
// Una parte de un mensaje que no cabe en la cola no se deja en el proxio (parts.js).
|
|
692
|
+
if (!w.queueFallback) {
|
|
693
|
+
this._emit('error', { type: 'client', error: 'the peer left in the middle of a message too big to wait in the proxy', code: 'too-big-for-queue', peerPubkey: w.peerPubkey })
|
|
694
|
+
return
|
|
695
|
+
}
|
|
608
696
|
try { this._sendByPubkeyRaw([w.peerPubkey], w.sobre) } catch (e) { this._emit('error', { type: 'client', error: e.message, code: e.code || null }) }
|
|
609
697
|
}
|
|
610
698
|
|
|
@@ -805,7 +893,16 @@ export class WebSocketProxyClient {
|
|
|
805
893
|
// Quién selló (la llave de cifrado), si el sobre lo dice: es lo que autentica al
|
|
806
894
|
// remitente, porque solo quien tiene esa privada pudo armarlo.
|
|
807
895
|
const senderEncPub = this.sealing?.senderOf?.(payload) || null
|
|
808
|
-
|
|
896
|
+
const fullMeta = { ...meta, sealed: true, ...(senderEncPub ? { senderEncPub } : {}) }
|
|
897
|
+
if (isPart(opened)) {
|
|
898
|
+
// Se agrupa por quien selló: solo esa llave pudo armar cada parte.
|
|
899
|
+
let whole
|
|
900
|
+
try { whole = this._parts.add(senderEncPub || meta.fromPubkey || from || '', opened) }
|
|
901
|
+
catch (e) { this._emit('error', { type: 'bad_part', from, code: e.code || 'bad-part', error: e }); return }
|
|
902
|
+
if (whole !== null) this._emit('message', from, whole, { ...fullMeta, parts: opened.__dotrino_part.n })
|
|
903
|
+
return
|
|
904
|
+
}
|
|
905
|
+
this._emit('message', from, opened, fullMeta)
|
|
809
906
|
} catch (e) {
|
|
810
907
|
// Sealed to somebody else, or tampered with. Staying quiet is the point.
|
|
811
908
|
this._emit('error', { type: 'undecipherable', from, error: e })
|
|
@@ -1242,11 +1339,21 @@ export class WebSocketProxyClient {
|
|
|
1242
1339
|
|
|
1243
1340
|
_open () {
|
|
1244
1341
|
registerTransport(this)
|
|
1342
|
+
// El intento anterior, si sigue a medio abrir, se suelta: sus eventos tardíos ya se
|
|
1343
|
+
// ignoran (`ws !== this.ws`), pero el socket quedaría abierto sin dueño.
|
|
1344
|
+
const prev = this.ws
|
|
1345
|
+
if (prev && prev.readyState === 0 /* CONNECTING */) {
|
|
1346
|
+
this.ws = null // antes de cerrarlo: su `close` ya no es de este cliente
|
|
1347
|
+
try { prev.close() } catch (_) {}
|
|
1348
|
+
}
|
|
1245
1349
|
const ws = new WebSocket(this.url)
|
|
1246
1350
|
this.ws = ws
|
|
1351
|
+
const openTimer = this.connectTimeout > 0 ? setTimeout(() => this._onConnectTimeout(ws), this.connectTimeout) : null
|
|
1352
|
+
openTimer?.unref?.()
|
|
1247
1353
|
// `ws !== this.ws` ⇒ es un socket que ya abandonamos (p.ej. por heartbeat
|
|
1248
1354
|
// muerto). Ignoramos sus eventos tardíos para no disparar reconexiones dobles.
|
|
1249
1355
|
ws.addEventListener('open', () => {
|
|
1356
|
+
if (openTimer) clearTimeout(openTimer)
|
|
1250
1357
|
if (ws !== this.ws) return
|
|
1251
1358
|
this._connected = true
|
|
1252
1359
|
this._reconnectAttempts = 0
|
|
@@ -1269,6 +1376,7 @@ export class WebSocketProxyClient {
|
|
|
1269
1376
|
}
|
|
1270
1377
|
})
|
|
1271
1378
|
ws.addEventListener('close', (ev) => {
|
|
1379
|
+
if (openTimer) clearTimeout(openTimer)
|
|
1272
1380
|
if (ws !== this.ws) return
|
|
1273
1381
|
this._stopHeartbeat()
|
|
1274
1382
|
const wasConnected = this._connected
|
|
@@ -1301,6 +1409,23 @@ export class WebSocketProxyClient {
|
|
|
1301
1409
|
})
|
|
1302
1410
|
}
|
|
1303
1411
|
|
|
1412
|
+
// El intento no abrió ni falló a tiempo: se suelta y se trata como un cierre fallido.
|
|
1413
|
+
_onConnectTimeout (ws) {
|
|
1414
|
+
if (ws !== this.ws || this._connected) return
|
|
1415
|
+
this.ws = null // a partir de acá, los eventos tardíos de `ws` se ignoran
|
|
1416
|
+
try { ws.close() } catch (_) {}
|
|
1417
|
+
const err = errorCon(`could not open the connection to ${this.url} in ${Math.round(this.connectTimeout / 1000)}s`, 'CONNECT_TIMEOUT')
|
|
1418
|
+
this._emit('error', { type: 'connect_timeout', error: err })
|
|
1419
|
+
if (this._connectReject) {
|
|
1420
|
+
this._connectReject(err)
|
|
1421
|
+
this._connectResolve = null
|
|
1422
|
+
this._connectReject = null
|
|
1423
|
+
}
|
|
1424
|
+
// Lo mismo que en `close`: se sigue reintentando solo si esto ya era un reintento. Un
|
|
1425
|
+
// `connect()` inicial que no abre se rechaza y ya.
|
|
1426
|
+
if (this._reconnectAttempts > 0 && this.autoReconnect) this._scheduleReconnect()
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1304
1429
|
// ---------- heartbeat ----------
|
|
1305
1430
|
|
|
1306
1431
|
_startHeartbeat () {
|
|
@@ -11,6 +11,7 @@ export {
|
|
|
11
11
|
} from './encpub.js'
|
|
12
12
|
|
|
13
13
|
export { listTransports, routeOf } from './stats.js'
|
|
14
|
+
export { PART_TAG, DIRECT_PART_CHARS, PROXY_PART_CHARS, QUEUE_MAX_CHARS, RELAY_MAX_CHARS } from './parts.js'
|
|
14
15
|
export { logStats, formatStats, netReport, splitPeers } from './stats-log.js'
|
|
15
16
|
|
|
16
17
|
import { WebSocketProxyClient } from './client.js'
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MENSAJES GRANDES, EN PARTES (0.33.0).
|
|
3
|
+
*
|
|
4
|
+
* Un mensaje dirigido tiene techo en cada camino: el canal directo (WebRTC) rechaza lo que
|
|
5
|
+
* pasa de 64 KiB hacia libwebrtc y la trama del proxio es de 1 MB. Antes, lo que no cabía
|
|
6
|
+
* en el canal directo se iba callado por el proxio (y llegaba desordenado), y lo que no
|
|
7
|
+
* cabía en la trama no salía. Ahora el pilar parte el mensaje y la otra punta lo rearma:
|
|
8
|
+
* la app manda y recibe un objeto entero, como siempre.
|
|
9
|
+
*
|
|
10
|
+
* La regla del dueño (2026-10-15), que es lo que decide los topes:
|
|
11
|
+
* - por el canal directo (WebRTC, también por TURN) NO HAY TOPE;
|
|
12
|
+
* - por el proxio EN VIVO (los dos conectados, sin canal directo), como mucho 25 MB de
|
|
13
|
+
* archivo: cada parte cruza el VPS dos veces, y uno de 25 MB ya son ~90 MB de tráfico.
|
|
14
|
+
* Más que eso lanza `too-big-for-relay`, y sale cuando haya canal directo;
|
|
15
|
+
* - lo que se queda ESPERANDO en el proxio (por pubkey, el otro desconectado) es como
|
|
16
|
+
* mucho 8 MB de archivo: una foto de teléfono pesa 2-5 MB, y con 1 MB (lo primero que se
|
|
17
|
+
* pensó) casi ninguna habría podido esperar. Lo que pasa de ahí no se deja en el proxio:
|
|
18
|
+
* se lanza `too-big-for-queue`.
|
|
19
|
+
*
|
|
20
|
+
* Cada parte va SELLADA por su cuenta (el pilar sella cada una como cualquier mensaje), así
|
|
21
|
+
* que el proxio no ve ni el contenido ni cuántas partes son de qué. El rearmado agrupa por la
|
|
22
|
+
* llave que selló: nadie más puede colar una parte en el mensaje de otro.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const PART_TAG = '__dotrino_part'
|
|
26
|
+
|
|
27
|
+
/** Parte por el canal directo: por debajo de los 64 KiB que acepta libwebrtc, ya sellada. */
|
|
28
|
+
export const DIRECT_PART_CHARS = 40 * 1024
|
|
29
|
+
/** Parte por el proxio: sellada (×1,37) cabe holgada en la trama de 1 MB. */
|
|
30
|
+
export const PROXY_PART_CHARS = 512 * 1024
|
|
31
|
+
/**
|
|
32
|
+
* Lo más que se deja esperando en el proxio. Se mide sobre el JSON del mensaje: 8 MB de
|
|
33
|
+
* archivo en base64 ocupan ~10,7 MB, y se deja margen para lo que va alrededor.
|
|
34
|
+
*/
|
|
35
|
+
export const QUEUE_MAX_CHARS = 11 * 1024 * 1024
|
|
36
|
+
/** Lo más que pasa por el proxio EN VIVO: 25 MB de archivo son ~33,4 MB en base64. */
|
|
37
|
+
export const RELAY_MAX_CHARS = 34 * 1024 * 1024
|
|
38
|
+
|
|
39
|
+
/** Rearmado: lo que se acepta guardar a medias, y cuánto se espera a la parte siguiente. */
|
|
40
|
+
const RECV_MAX_CHARS = 512 * 1024 * 1024
|
|
41
|
+
const RECV_IDLE_MS = 5 * 60 * 1000
|
|
42
|
+
const MAX_PARTS = 1 << 20
|
|
43
|
+
|
|
44
|
+
/** El id de un mensaje en partes: al azar, para que dos mensajes no se mezclen. */
|
|
45
|
+
export function partId () {
|
|
46
|
+
const b = new Uint8Array(12)
|
|
47
|
+
globalThis.crypto.getRandomValues(b)
|
|
48
|
+
return Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Las partes (ya como objetos para sellar) de un JSON de `json.length` > `size`. */
|
|
52
|
+
export function splitParts (json, size) {
|
|
53
|
+
const n = Math.ceil(json.length / size)
|
|
54
|
+
const id = partId()
|
|
55
|
+
const out = []
|
|
56
|
+
for (let i = 0; i < n; i++) out.push({ [PART_TAG]: { id, i, n }, d: json.slice(i * size, (i + 1) * size) })
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const isPart = (o) => !!(o && typeof o === 'object' && o[PART_TAG])
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Rearma los mensajes. `add` devuelve el mensaje entero cuando llega la última parte, `null`
|
|
64
|
+
* mientras falten, y lanza con `code` si la parte no vale (se dice: una parte mala no puede
|
|
65
|
+
* desaparecer en silencio, porque el mensaje entero se pierde con ella).
|
|
66
|
+
*/
|
|
67
|
+
export class Reassembler {
|
|
68
|
+
constructor ({ onExpire = () => {} } = {}) {
|
|
69
|
+
this._open = new Map()
|
|
70
|
+
this._chars = 0
|
|
71
|
+
this._onExpire = onExpire
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @param {string} sender quién selló (agrupa) @param {any} part */
|
|
75
|
+
add (sender, part) {
|
|
76
|
+
const p = part[PART_TAG]
|
|
77
|
+
const d = part.d
|
|
78
|
+
if (!p || typeof p.id !== 'string' || p.id.length > 64 ||
|
|
79
|
+
!Number.isInteger(p.n) || !Number.isInteger(p.i) || p.n < 2 || p.n > MAX_PARTS ||
|
|
80
|
+
p.i < 0 || p.i >= p.n || typeof d !== 'string') {
|
|
81
|
+
throw Object.assign(new Error('malformed message part'), { code: 'bad-part' })
|
|
82
|
+
}
|
|
83
|
+
const key = `${sender}|${p.id}`
|
|
84
|
+
let a = this._open.get(key)
|
|
85
|
+
if (!a) {
|
|
86
|
+
a = { n: p.n, got: new Array(p.n), count: 0, chars: 0, timer: null }
|
|
87
|
+
this._open.set(key, a)
|
|
88
|
+
}
|
|
89
|
+
if (a.n !== p.n) {
|
|
90
|
+
this._drop(key)
|
|
91
|
+
throw Object.assign(new Error('message parts disagree on their count'), { code: 'bad-part' })
|
|
92
|
+
}
|
|
93
|
+
if (a.got[p.i] === undefined) {
|
|
94
|
+
if (this._chars + d.length > RECV_MAX_CHARS) {
|
|
95
|
+
this._drop(key)
|
|
96
|
+
throw Object.assign(new Error('too much of a message in parts is pending'), { code: 'parts-too-big' })
|
|
97
|
+
}
|
|
98
|
+
a.got[p.i] = d
|
|
99
|
+
a.count++
|
|
100
|
+
a.chars += d.length
|
|
101
|
+
this._chars += d.length
|
|
102
|
+
}
|
|
103
|
+
if (a.timer) clearTimeout(a.timer)
|
|
104
|
+
if (a.count === a.n) {
|
|
105
|
+
this._drop(key)
|
|
106
|
+
try { return JSON.parse(a.got.join('')) }
|
|
107
|
+
catch (_) { throw Object.assign(new Error('a message in parts did not join into JSON'), { code: 'bad-part' }) }
|
|
108
|
+
}
|
|
109
|
+
a.timer = setTimeout(() => {
|
|
110
|
+
const missing = a.n - a.count
|
|
111
|
+
this._drop(key)
|
|
112
|
+
this._onExpire({ id: p.id, parts: a.n, missing })
|
|
113
|
+
}, RECV_IDLE_MS)
|
|
114
|
+
a.timer.unref?.()
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_drop (key) {
|
|
119
|
+
const a = this._open.get(key)
|
|
120
|
+
if (!a) return
|
|
121
|
+
if (a.timer) clearTimeout(a.timer)
|
|
122
|
+
this._chars -= a.chars
|
|
123
|
+
this._open.delete(key)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
clear () { for (const k of [...this._open.keys()]) this._drop(k) }
|
|
127
|
+
}
|
|
@@ -192,6 +192,29 @@ export class WebRTCManager {
|
|
|
192
192
|
return false
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Mandar por el canal directo ESPERANDO a que su búfer baje. Para las partes de un
|
|
197
|
+
* mensaje grande: soltarlas todas de golpe desborda el búfer del canal, el navegador lo
|
|
198
|
+
* cierra y el resto se pierde. Devuelve false si el canal no está (o se cerró), y quien
|
|
199
|
+
* llama sigue por el proxio.
|
|
200
|
+
*/
|
|
201
|
+
async sendDirect (to, payloadString, highWater = 1024 * 1024) {
|
|
202
|
+
const peer = this.peers.get(to)
|
|
203
|
+
const dc = peer && peer.dc
|
|
204
|
+
if (!dc || dc.readyState !== 'open') return false
|
|
205
|
+
while (dc.bufferedAmount > highWater) {
|
|
206
|
+
await new Promise((r) => setTimeout(r, 15))
|
|
207
|
+
if (dc.readyState !== 'open') return false
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
dc.send(payloadString)
|
|
211
|
+
this.count?.('out', to, utf8Length(payloadString), peer.route || 'webrtc')
|
|
212
|
+
return true
|
|
213
|
+
} catch (_) {
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
195
218
|
/** Optional: explicitly preconnect to a peer. */
|
|
196
219
|
connect (to) {
|
|
197
220
|
const peer = this._ensurePeer(to)
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
Copia vendorizada de @dotrino/roadmap@0.4.17 (dotrino-roadmap/src/{index,manifests}.js).
|
|
2
|
+
NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
|
|
3
|
+
El registro de versiones rotas que viaja con cada versión. Importa @dotrino/compat/ranges.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dotrino/roadmap — EL REGISTRO COMÚN, COMO PIEZA DE CONSULTA.
|
|
3
|
+
*
|
|
4
|
+
* Los manifiestos de este repo dicen **qué versión va cada producto, qué protocolo habla,
|
|
5
|
+
* qué rangos necesita de los demás y qué versiones se saben rotas**. Esta librería es la
|
|
6
|
+
* forma de leerlos: cada producto la importa en vez de abrir el JSON a mano, porque
|
|
7
|
+
* treinta piezas parseando el mismo archivo a su manera son treinta formas de equivocarse
|
|
8
|
+
* y ninguna hace ruido cuando se equivoca.
|
|
9
|
+
*
|
|
10
|
+
* REPARTO DE TRABAJO, para que no se mezcle:
|
|
11
|
+
*
|
|
12
|
+
* · **`@dotrino/compat`** decide. Es la lógica: declarar, comparar, avisar.
|
|
13
|
+
* · **esto** es el registro. Son los datos, y se editan aquí.
|
|
14
|
+
*
|
|
15
|
+
* Va como `peerDependency` de `@dotrino/compat` a propósito: un pilar dentro de otro se
|
|
16
|
+
* cuela anidado a cada consumidor, y ya pasó (`PENDIENTES.md`, «pilar anidado»).
|
|
17
|
+
*
|
|
18
|
+
* EL MANIFIESTO ES POLÍTICA DEL CÓDIGO (dueño, 2026-09-04): viaja con la versión que lo
|
|
19
|
+
* trae. Lo que llega por la red es el **aviso** firmado de `@dotrino/compat/advisory`, que
|
|
20
|
+
* suma incompatibilidades y no quita ninguna.
|
|
21
|
+
*/
|
|
22
|
+
import { satisfies } from '@dotrino/compat/ranges'
|
|
23
|
+
import { MANIFESTS } from './manifests.js'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* NADA DE DISCO, y esto costó un binario que no arrancaba.
|
|
27
|
+
*
|
|
28
|
+
* La primera versión leía los JSON con `fileURLToPath(import.meta.url)` calculado AL
|
|
29
|
+
* IMPORTARSE. En un ejecutable único eso no existe: la bóveda murió al arrancar con «The
|
|
30
|
+
* "path" argument must be of type string», y en un navegador no habría arrancado nunca.
|
|
31
|
+
*
|
|
32
|
+
* Los consumen ~30 PWAs, varios daemons y un binario. Un registro que hay que leer del
|
|
33
|
+
* disco no sirve para eso, así que los manifiestos van DENTRO del código
|
|
34
|
+
* (`src/manifests.js`, generado desde `manifests/*.json`).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Los manifiestos que trae esta versión. Hay uno («dotrino») y caben varios. */
|
|
38
|
+
export function manifestNames () {
|
|
39
|
+
return Object.keys(MANIFESTS).sort()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Un manifiesto por nombre. LANZA si no está: quien pregunta por un registro que no existe
|
|
44
|
+
* tiene un error, y devolverle un objeto vacío lo convertiría en «todo compatible».
|
|
45
|
+
*/
|
|
46
|
+
export function loadManifest (name = 'dotrino') {
|
|
47
|
+
const m = Object.hasOwn(MANIFESTS, String(name)) ? MANIFESTS[String(name)] : null
|
|
48
|
+
if (!m) throw new Error(`roadmap: there is no manifest called "${name}"`)
|
|
49
|
+
return m
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Los productos que el registro conoce. */
|
|
53
|
+
export const products = (m) => Object.keys(m?.products || {}).sort()
|
|
54
|
+
|
|
55
|
+
/** La ficha de un producto, o `null` si el registro no lo conoce. */
|
|
56
|
+
export const productOf = (m, product) => m?.products?.[product] || null
|
|
57
|
+
|
|
58
|
+
/** La versión que el registro da por vigente. `null` si no lo conoce. */
|
|
59
|
+
export const currentOf = (m, product) => productOf(m, product)?.current || null
|
|
60
|
+
|
|
61
|
+
/** El rango que `product` necesita de `peer`, o `null` si no exige nada. */
|
|
62
|
+
export function requirementFor (m, product, peer) {
|
|
63
|
+
const p = productOf(m, product)
|
|
64
|
+
if (!p) return null
|
|
65
|
+
return p.requires?.[peer] ?? null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* ¿La versión de `peer` cumple lo que `product` necesita?
|
|
70
|
+
*
|
|
71
|
+
* Tres respuestas y ninguna es «bueno, adelante»:
|
|
72
|
+
* · `true` — cumple.
|
|
73
|
+
* · `false` — no cumple, y `reason` dice qué se esperaba.
|
|
74
|
+
* · cuando el registro NO exige nada, cumple: no exigir es una decisión, no una laguna.
|
|
75
|
+
*
|
|
76
|
+
* Si el registro no conoce al producto que pregunta, es `false`: preguntar por quien no
|
|
77
|
+
* está en el registro no puede salir que sí.
|
|
78
|
+
*/
|
|
79
|
+
export function meets (m, { product, peer, version }) {
|
|
80
|
+
if (!productOf(m, product)) {
|
|
81
|
+
return { ok: false, reason: `the registry does not know "${product}"` }
|
|
82
|
+
}
|
|
83
|
+
const rango = requirementFor(m, product, peer)
|
|
84
|
+
if (rango == null) return { ok: true, reason: '' }
|
|
85
|
+
if (satisfies(version, rango)) return { ok: true, reason: '' }
|
|
86
|
+
return { ok: false, reason: `${product} needs ${peer} ${rango} and this one is ${version}` }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** La lista de rotas del registro, lista para pasarla a `check` de `@dotrino/compat`. */
|
|
90
|
+
export const brokenOf = (m) => Array.isArray(m?.broken) ? m.broken : []
|
|
91
|
+
|
|
92
|
+
/** Lo que el administrador enseña: qué versión debería ir cada pieza, según el registro. */
|
|
93
|
+
export function summary (m) {
|
|
94
|
+
return {
|
|
95
|
+
manifest: m?.manifest || null,
|
|
96
|
+
seq: m?.seq ?? null,
|
|
97
|
+
issued: m?.issued || null,
|
|
98
|
+
products: products(m).map((p) => ({
|
|
99
|
+
product: p,
|
|
100
|
+
current: currentOf(m, p),
|
|
101
|
+
protocol: productOf(m, p)?.protocol ?? null,
|
|
102
|
+
requires: productOf(m, p)?.requires || {}
|
|
103
|
+
})),
|
|
104
|
+
broken: brokenOf(m)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default { manifestNames, loadManifest, products, productOf, currentOf, requirementFor, meets, brokenOf, summary }
|