@dotrino/identity 0.116.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/identity",
3
- "version": "0.116.0",
3
+ "version": "0.117.0",
4
4
  "description": "Identidad y rating de usuarios compartidos entre apps de Dotrino (vault iframe + postMessage)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/vault/content.js CHANGED
@@ -133,6 +133,33 @@ export async function encryptWithCek ({ cek, gen, plaintext }) {
133
133
  return { gen, iv: b64(iv), ct: b64(ct) }
134
134
  }
135
135
 
136
+ /**
137
+ * Cifra BYTES con una clave simétrica, y devuelve bytes: `{ iv, ct }` como `Uint8Array`.
138
+ *
139
+ * Es la hermana binaria de `encryptWithCek`, para lo que no tiene por qué pasar por texto:
140
+ * un archivo de 5 MB cifrado y en base64 ocupa 6,7 MB, y si además iba dentro de un JSON en
141
+ * base64, 8,9 MB. Así viaja con lo que ocupa. Lo usa el transporte para los mensajes con
142
+ * archivos: el archivo se cifra con una CEK nueva y solo la CEK pasa por el sellado de
143
+ * siempre (`wrapForMember`, o la bóveda).
144
+ *
145
+ * @param {{ cek: string, bytes: Uint8Array }} o
146
+ * @returns {Promise<{ iv: Uint8Array, ct: Uint8Array }>}
147
+ */
148
+ export async function encryptBytesWithCek ({ cek, bytes }) {
149
+ if (!(bytes instanceof Uint8Array)) throw new Error('encryptBytesWithCek: bytes must be a Uint8Array')
150
+ const k = await subtle.importKey('raw', fromB64(cek), { name: 'AES-GCM' }, false, ['encrypt'])
151
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12))
152
+ const ct = new Uint8Array(await subtle.encrypt({ name: 'AES-GCM', iv }, k, bytes))
153
+ return { iv, ct }
154
+ }
155
+
156
+ /** El inverso de `encryptBytesWithCek`. Lanza si los bytes no son de esa clave o se tocaron. */
157
+ export async function decryptBytesWithCek ({ cek, iv, ct }) {
158
+ if (!(iv instanceof Uint8Array) || !(ct instanceof Uint8Array)) throw new Error('decryptBytesWithCek: iv and ct must be Uint8Array')
159
+ const k = await subtle.importKey('raw', fromB64(cek), { name: 'AES-GCM' }, false, ['decrypt'])
160
+ return new Uint8Array(await subtle.decrypt({ name: 'AES-GCM', iv }, k, ct))
161
+ }
162
+
136
163
  /** Cierra con la LLAVE DE LA CUENTA (generación `gen`): el sobre lleva la marca. */
137
164
  export async function sealAccount ({ cek, gen, plaintext }) {
138
165
  if (!Number.isInteger(gen) || gen < 1) throw new Error('sealAccount: the account key generation starts at 1')
@@ -243,5 +270,5 @@ export async function resealStale (value, { gen, reseal, now = Date.now() }) {
243
270
 
244
271
  export default {
245
272
  makeContentKey, wrapForMember, openWrap, makeGeneration, myContentKey,
246
- encryptWithCek, sealAccount, decryptWithCek, decryptWithKeyring, CEK_ENVELOPE, isCekEnvelope, isLegacyCekEnvelope, LEGACY_UNMARKED_UNTIL, resealStale
273
+ encryptWithCek, encryptBytesWithCek, decryptBytesWithCek, sealAccount, decryptWithCek, decryptWithKeyring, CEK_ENVELOPE, isCekEnvelope, isLegacyCekEnvelope, LEGACY_UNMARKED_UNTIL, resealStale
247
274
  }
@@ -1,4 +1,4 @@
1
- Copia vendorizada de @dotrino/proxy-client@0.32.2 (dotrino-proxy-client/src/{canonical,client,encpub,index,sealing,signature,stats-log,stats,webrtc}.js).
1
+ Copia vendorizada de @dotrino/proxy-client@0.34.0 (dotrino-proxy-client/src/{canonical,client,encpub,index,parts,sealing,signature,stats-log,stats,webrtc}.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.
@@ -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.
@@ -186,7 +187,12 @@ export class WebSocketProxyClient {
186
187
  this._handlers = new Map()
187
188
  this._pending = new Map() // messageId -> { resolve, reject, timer }
188
189
  this._nextId = 1
189
- 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
+ })
190
196
  this._traffic = new TrafficStats()
191
197
 
192
198
  this._rtc = this.enableWebRTC ? new WebRTCManager({
@@ -501,10 +507,19 @@ export class WebSocketProxyClient {
501
507
  */
502
508
  async sendSealed (toPubkeys, payload, { peerEncPub, ...opts } = {}) {
503
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]
504
517
  // Con la llave puesta a mano se respeta tal cual: quien la pasa está diciendo que ya
505
518
  // sabe de quién es (se emparejaron), y un sobre vale para todos los destinatarios.
506
519
  if (peerEncPub) {
507
- this._sendByPubkeyRaw(list, await this._seal(payload, peerEncPub, list.length === 1 ? list[0] : undefined), opts)
520
+ for (const piece of pieces) {
521
+ this._sendByPubkeyRaw(list, await this._seal(piece, peerEncPub, list.length === 1 ? list[0] : undefined), opts)
522
+ }
508
523
  return
509
524
  }
510
525
  // Sin ella, se averigua. UNA ENVOLTURA POR DESTINATARIO: cada uno tiene su llave, así
@@ -515,7 +530,7 @@ export class WebSocketProxyClient {
515
530
  // sin forma de saber cuál mitad.
516
531
  const llaves = await Promise.all(list.map(async (pk) => [pk, await this.encPubOf(pk)]))
517
532
  for (const [pk, encPub] of llaves) {
518
- this._sendByPubkeyRaw([pk], await this._seal(payload, encPub, pk), opts)
533
+ for (const piece of pieces) this._sendByPubkeyRaw([pk], await this._seal(piece, encPub, pk), opts)
519
534
  }
520
535
  }
521
536
 
@@ -547,10 +562,70 @@ export class WebSocketProxyClient {
547
562
  peerEncPub = await this.encPubOf(peerPubkey)
548
563
  }
549
564
  if (!peerPubkey && tokens.length === 1) peerPubkey = this.pubkeyOfToken(tokens[0]) || undefined
550
- const sobre = await this._seal(payload, peerEncPub, peerPubkey)
551
- if (tokens.length === 1 && peerPubkey) { this._sendToTokenOrQueue(tokens[0], sobre, peerPubkey); return }
552
- // Por `send`, no por `_sendRaw`: así sigue prefiriendo el canal directo si lo hay.
553
- this.send(tokens, sobre)
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))
554
629
  }
555
630
 
556
631
  /**
@@ -585,16 +660,16 @@ export class WebSocketProxyClient {
585
660
  * `token_gone` para que la app deje de usar ese token. No bloquea a nadie: se manda ya y
586
661
  * el arreglo, si hace falta, llega detrás.
587
662
  */
588
- _sendToTokenOrQueue (token, sobre, peerPubkey) {
663
+ _sendToTokenOrQueue (token, sobre, peerPubkey, { queueFallback = true, skipDirect = false } = {}) {
589
664
  if (this.requireSealed && !this._isSealed(sobre)) {
590
665
  throw errorCon('requireSealed: refusing to send a directed message in the clear — use sendSealedTo()', 'unsealed')
591
666
  }
592
667
  const messageStr = JSON.stringify(sobre)
593
- if (this._rtc && this._rtc.trySend(token, messageStr)) return
668
+ if (!skipDirect && this._rtc && this._rtc.trySend(token, messageStr)) return
594
669
  const id = `msg_${this._nextId++}`
595
670
  const timer = setTimeout(() => this._tokenWatch.delete(id), 15000)
596
671
  timer.unref?.()
597
- this._tokenWatch.set(id, { token, sobre, peerPubkey, timer })
672
+ this._tokenWatch.set(id, { token, sobre, peerPubkey, timer, queueFallback })
598
673
  try {
599
674
  this._sendRaw({ to: [token], message: messageStr, id })
600
675
  } catch (e) {
@@ -613,6 +688,11 @@ export class WebSocketProxyClient {
613
688
  this._tokenWatch.delete(data.id)
614
689
  if (!Array.isArray(data.failed) || !data.failed.includes(w.token)) return
615
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
+ }
616
696
  try { this._sendByPubkeyRaw([w.peerPubkey], w.sobre) } catch (e) { this._emit('error', { type: 'client', error: e.message, code: e.code || null }) }
617
697
  }
618
698
 
@@ -813,7 +893,16 @@ export class WebSocketProxyClient {
813
893
  // Quién selló (la llave de cifrado), si el sobre lo dice: es lo que autentica al
814
894
  // remitente, porque solo quien tiene esa privada pudo armarlo.
815
895
  const senderEncPub = this.sealing?.senderOf?.(payload) || null
816
- this._emit('message', from, opened, { ...meta, sealed: true, ...(senderEncPub ? { senderEncPub } : {}) })
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)
817
906
  } catch (e) {
818
907
  // Sealed to somebody else, or tampered with. Staying quiet is the point.
819
908
  this._emit('error', { type: 'undecipherable', from, error: e })
@@ -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)
@@ -1,3 +1,3 @@
1
- Copia vendorizada de @dotrino/roadmap@0.4.13 (dotrino-roadmap/src/{index,manifests}.js).
1
+ Copia vendorizada de @dotrino/roadmap@0.4.17 (dotrino-roadmap/src/{index,manifests}.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
  El registro de versiones rotas que viaja con cada versión. Importa @dotrino/compat/ranges.
@@ -31,7 +31,7 @@ export const MANIFESTS = Object.freeze({
31
31
  "identity": {
32
32
  "repo": "imdotrino/dotrino-identity",
33
33
  "npm": "@dotrino/identity",
34
- "current": "0.115.0",
34
+ "current": "0.116.0",
35
35
  "protocol": 1,
36
36
  "requires": {}
37
37
  },
@@ -47,13 +47,13 @@ export const MANIFESTS = Object.freeze({
47
47
  },
48
48
  "messenger": {
49
49
  "repo": "imdotrino/dotrino-messenger",
50
- "current": "0.5.2",
50
+ "current": "0.7.0",
51
51
  "protocol": 2,
52
52
  "requires": {
53
53
  "identity": ">=0.108.0",
54
- "proxy-client": ">=0.28.0",
54
+ "proxy-client": ">=0.34.0",
55
55
  "store": ">=0.12.3",
56
- "proxy": "1.1.0+"
56
+ "proxy": "1.5.2+"
57
57
  }
58
58
  },
59
59
  "messenger-android": {
@@ -107,18 +107,19 @@ export const MANIFESTS = Object.freeze({
107
107
  },
108
108
  "proxy": {
109
109
  "repo": "imdotrino/dotrino-proxy",
110
- "current": "1.4.9",
110
+ "current": "1.5.2",
111
111
  "protocol": 2,
112
112
  "requires": {
113
- "vault": ">=0.79.0",
114
- "identity": ">=0.99.0",
115
- "proxy-client": ">=0.23.1"
113
+ "vault": ">=0.86.0",
114
+ "identity": ">=0.116.0",
115
+ "proxy-client": ">=0.23.1",
116
+ "content": "0.5.3+"
116
117
  }
117
118
  },
118
119
  "proxy-client": {
119
120
  "repo": "imdotrino/dotrino-proxy-client",
120
121
  "npm": "@dotrino/proxy-client",
121
- "current": "0.32.2",
122
+ "current": "0.34.0",
122
123
  "protocol": 2,
123
124
  "requires": {
124
125
  "identity": ">=0.53.0",
@@ -128,11 +129,11 @@ export const MANIFESTS = Object.freeze({
128
129
  "remote-agent": {
129
130
  "repo": "imdotrino/dotrino-remote-agent",
130
131
  "npm": "@dotrino/remote-agent",
131
- "current": "0.19.0",
132
+ "current": "0.19.1",
132
133
  "protocol": 1,
133
134
  "requires": {
134
- "identity": ">=0.112.0",
135
- "vault": ">=0.64.0",
135
+ "identity": ">=0.115.0",
136
+ "vault": ">=0.85.0",
136
137
  "proxy-client": ">=0.30.0",
137
138
  "vaultd": ">=0.154.0"
138
139
  }
@@ -140,12 +141,12 @@ export const MANIFESTS = Object.freeze({
140
141
  "terminal-agent": {
141
142
  "repo": "imdotrino/dotrino-terminal",
142
143
  "npm": "@dotrino/terminal-agent",
143
- "current": "0.47.1",
144
+ "current": "0.47.4",
144
145
  "protocol": 2,
145
146
  "requires": {
146
147
  "remote-agent": ">=0.18.3",
147
- "identity": ">=0.92.0",
148
- "vault": ">=0.79.0",
148
+ "identity": ">=0.116.0",
149
+ "vault": ">=0.86.0",
149
150
  "proxy-client": ">=0.31.1",
150
151
  "vaultd": ">=0.142.0"
151
152
  }
@@ -168,7 +169,7 @@ export const MANIFESTS = Object.freeze({
168
169
  },
169
170
  "terminal-desktop": {
170
171
  "repo": "imdotrino/dotrino-terminal",
171
- "current": "0.2.72",
172
+ "current": "0.2.74",
172
173
  "protocol": 1,
173
174
  "requires": {
174
175
  "terminal-agent": ">=0.37.0"
@@ -189,31 +190,31 @@ export const MANIFESTS = Object.freeze({
189
190
  "content": {
190
191
  "repo": "imdotrino/dotrino-content",
191
192
  "npm": "@dotrino/content",
192
- "current": "0.5.1",
193
+ "current": "0.5.3",
193
194
  "protocol": 1,
194
195
  "requires": {
195
196
  "remote-agent": ">=0.13.0",
196
- "identity": ">=0.92.0",
197
- "vault": ">=0.79.0",
197
+ "identity": ">=0.116.0",
198
+ "vault": ">=0.86.0",
198
199
  "proxy-client": ">=0.23.1"
199
200
  }
200
201
  },
201
202
  "sealers": {
202
203
  "repo": "imdotrino/dotrino-sealers",
203
204
  "npm": "@dotrino/sealers",
204
- "current": "0.9.1",
205
+ "current": "0.9.2",
205
206
  "protocol": 1,
206
207
  "requires": {
207
208
  "remote-agent": ">=0.13.0",
208
- "identity": ">=0.92.0",
209
- "vault": ">=0.75.0",
209
+ "identity": ">=0.116.0",
210
+ "vault": ">=0.86.0",
210
211
  "proxy-client": ">=0.23.1"
211
212
  }
212
213
  },
213
214
  "roadmap": {
214
215
  "repo": "imdotrino/dotrino-roadmap",
215
216
  "npm": "@dotrino/roadmap",
216
- "current": "0.4.8",
217
+ "current": "0.4.14",
217
218
  "protocol": 1,
218
219
  "requires": {
219
220
  "compat": "0.4.0+"
@@ -249,28 +250,32 @@ export const MANIFESTS = Object.freeze({
249
250
  "vault": {
250
251
  "repo": "imdotrino/dotrino-vault",
251
252
  "npm": "@dotrino/vault",
252
- "current": "0.85.0",
253
+ "current": "0.86.0",
253
254
  "protocol": 1,
254
255
  "requires": {
255
256
  "vaultd": ">=0.105.0",
256
257
  "proxy-client": "0.24.0+",
257
- "identity": ">=0.115.0",
258
- "opaque": ">=0.1.0"
258
+ "identity": ">=0.116.0",
259
+ "opaque": ">=0.1.0",
260
+ "compat": ">=0.4.0",
261
+ "roadmap": ">=0.4.13",
262
+ "passmanager": ">=0.17.0",
263
+ "store": ">=0.11.0"
259
264
  }
260
265
  },
261
266
  "vaultd": {
262
267
  "repo": "imdotrino/dotrino-vault",
263
268
  "npm": "@dotrino/vaultd",
264
- "current": "0.155.0",
269
+ "current": "0.157.0",
265
270
  "protocol": 1,
266
271
  "requires": {
267
- "identity": ">=0.115.0",
272
+ "identity": ">=0.116.0",
268
273
  "proxy-client": ">=0.30.1",
269
274
  "store": ">=0.11.0",
270
275
  "passmanager": ">=0.17.0",
271
276
  "compat": ">=0.4.0",
272
277
  "opaque": ">=0.1.0",
273
- "roadmap": ">=0.3.19"
278
+ "roadmap": ">=0.4.13"
274
279
  }
275
280
  },
276
281
  "verifier": {
@@ -312,12 +317,12 @@ export const MANIFESTS = Object.freeze({
312
317
  },
313
318
  "passmanager-extension": {
314
319
  "repo": "imdotrino/dotrino-passmanager",
315
- "current": "0.48.4",
320
+ "current": "0.49.1",
316
321
  "protocol": 1,
317
322
  "requires": {
318
- "identity": ">=0.109.0",
319
- "proxy-client": ">=0.30.4",
320
- "vault": ">=0.82.1",
323
+ "identity": ">=0.116.0",
324
+ "proxy-client": ">=0.32.2",
325
+ "vault": ">=0.86.0",
321
326
  "proxy": "1.1.0+"
322
327
  }
323
328
  },
@@ -333,11 +338,11 @@ export const MANIFESTS = Object.freeze({
333
338
  },
334
339
  "social-bot": {
335
340
  "repo": "imdotrino/dotrino-social-bot",
336
- "current": "0.2.0",
341
+ "current": "0.2.1",
337
342
  "protocol": 1,
338
343
  "requires": {
339
- "vault": ">=0.75.0",
340
- "identity": ">=0.92.0",
344
+ "vault": ">=0.86.0",
345
+ "identity": ">=0.116.0",
341
346
  "proxy-client": ">=0.23.1",
342
347
  "remote-agent": ">=0.13.0"
343
348
  }