@dotrino/identity 0.121.0 → 0.121.2

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.121.0",
3
+ "version": "0.121.2",
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/remote.js CHANGED
@@ -109,13 +109,27 @@ async function redeemCode (client, code) {
109
109
  return r.instance
110
110
  }
111
111
 
112
+ /**
113
+ * ¿LO DICE MI BÓVEDA, DE ESTA SESIÓN? Lo que va a la cuenta lo reciben todas sus bóvedas y
114
+ * cualquiera que lo vea pasar puede contestar. Un reto o un error solo cuentan firmados con la
115
+ * maestra de la invitación (`iss`) y con el `sn` de esta sesión (2026-10-18: sin esto, un
116
+ * `vault.error` de cualquiera tumbaba el alta). Mismo contrato que `fromMyVault` en
117
+ * `@dotrino/vault/service`.
118
+ */
119
+ async function fromMyVault (p, qr) {
120
+ const b = p?.signed
121
+ if (!p?.vsig || !b || typeof b !== 'object' || !qr?.iss) return false
122
+ if (b.iss !== qr.iss || b.sn !== qr.sn || b.type !== p.type) return false
123
+ try { return await verifyDeviceSig({ publickey: qr.iss, data: b, signature: p.vsig }) } catch (_) { return false }
124
+ }
125
+
112
126
  /** «¿Quién eres?» del QR corto: devuelve `{ iss, proxy, acct, m }` de la bóveda. */
113
127
  async function askVault (client, qr) {
114
128
  const target = await redeemCode(client, qr.conn)
115
129
  return new Promise((resolve, reject) => {
116
130
  const off = client.on('message', (_from, p) => {
131
+ // Un error aquí no se puede verificar (aún no se sabe quién es la bóveda): no cuenta.
117
132
  if (p?.type === MSG.HELLO_OK) { done(); verifyHello(p, qr.sn).then((b) => resolve({ iss: b.iss, proxy: b.proxy || qr.proxy, acct: b.acct || '', m: b.m || qr.m }), reject) }
118
- else if (p?.type === MSG.ERROR) { done(); reject(new Error(p.error)) }
119
133
  })
120
134
  const t = setTimeout(() => { done(); reject(new Error('the vault did not answer: that code may have expired')) }, 15000)
121
135
  const done = () => { off(); clearTimeout(t) }
@@ -179,8 +193,9 @@ export async function enrollDevice ({ qr, device, onChallenge, label = '', conti
179
193
 
180
194
  const enrolled = new Promise((resolve, reject) => {
181
195
  let sellando = false
182
- const off = client.on('message', (_from, p) => {
196
+ const off = client.on('message', async (_from, p) => {
183
197
  if (!p || typeof p !== 'object') return
198
+ if ((p.type === MSG.ENROLL_CHALLENGE || p.type === MSG.ERROR) && !await fromMyVault(p, qr)) return
184
199
  if (p.type === MSG.ENROLL_CHALLENGE) { try { onChallenge?.({ deviceId, code }) } catch (_) {} }
185
200
  // El vault ECHA el código que tipeaste; aceptamos SOLO si coincide con el que generamos.
186
201
  // (Un código distinto = un vault que no lo conoce → lo ignoramos y seguimos esperando.)
@@ -1,3 +1,3 @@
1
- Copia vendorizada de @dotrino/passmanager@0.18.4 (dotrino-passmanager/lib/src/{crypto,fields,generate,import,index,match,model,pubkey,sealed/convert,sealed/device,sealed/entry,sealed/index,sealed/keys,sealed/recovery,sealed/store,session-cache,totp,transport/protocol,transport/proxy,transport/sealed,vault/approval,vault/errors,vault/guard,vault/interface,vault/local,vault/remote,vault/responder,vault/sealed-local,vault/sealed-responder,vault/sealed,webauthn}.js).
1
+ Copia vendorizada de @dotrino/passmanager@0.18.5 (dotrino-passmanager/lib/src/{crypto,fields,generate,import,index,match,model,pubkey,sealed/convert,sealed/device,sealed/entry,sealed/index,sealed/keys,sealed/recovery,sealed/store,session-cache,totp,transport/protocol,transport/proxy,transport/sealed,vault/approval,vault/errors,vault/guard,vault/interface,vault/local,vault/remote,vault/responder,vault/sealed-local,vault/sealed-responder,vault/sealed,webauthn}.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 sobre de las contraseñas, el mismo que usan el binario y la extensión. Importa @dotrino/identity/content.
@@ -65,7 +65,7 @@ export function generatePassword ({
65
65
  /** Frase de varias palabras, más fácil de dictar o teclear a mano. */
66
66
  export function generatePassphrase (words, { count = 4, separator = '-' } = {}) {
67
67
  if (!Array.isArray(words) || words.length < 16) {
68
- throw new Error('generatePassphrase: hace falta una lista de palabras')
68
+ throw new Error('generatePassphrase: a word list is required')
69
69
  }
70
70
  return Array.from({ length: Math.max(2, count) }, () => words[randomBelow(words.length)])
71
71
  .join(separator)
@@ -111,7 +111,7 @@ export function fromBitwardenCsv (text) {
111
111
  export function fromBitwardenJson (json) {
112
112
  const data = typeof json === 'string' ? JSON.parse(json) : json
113
113
  if (data?.encrypted) {
114
- const e = new Error('el export de Bitwarden está cifrado: expórtalo sin cifrar')
114
+ const e = new Error('the Bitwarden export is encrypted: export it unencrypted')
115
115
  e.code = 'encrypted-export'
116
116
  throw e
117
117
  }
@@ -136,7 +136,7 @@ export function fromBitwardenJson (json) {
136
136
  */
137
137
  export function importAuto (text) {
138
138
  const t = String(text || '').trim()
139
- if (!t) return { format: 'vacío', entries: [] }
139
+ if (!t) return { format: 'empty', entries: [] }
140
140
 
141
141
  if (t.startsWith('{')) return { format: 'bitwarden-json', entries: fromBitwardenJson(t) }
142
142
 
@@ -150,7 +150,7 @@ export function importAuto (text) {
150
150
  if (head.includes('name') && head.includes('url') && head.includes('password')) {
151
151
  return { format: 'chrome-csv', entries: fromChromeCsv(t) }
152
152
  }
153
- const e = new Error('no reconozco el formato del archivo')
153
+ const e = new Error('unrecognized file format')
154
154
  e.code = 'unknown-format'
155
155
  throw e
156
156
  }
@@ -13,7 +13,7 @@ export function base32Decode (input) {
13
13
  const out = []
14
14
  for (const ch of s) {
15
15
  const idx = B32.indexOf(ch)
16
- if (idx === -1) throw new Error('base32: carácter inválido')
16
+ if (idx === -1) throw new Error('base32: invalid character')
17
17
  value = (value << 5) | idx
18
18
  bits += 5
19
19
  if (bits >= 8) {
@@ -38,12 +38,12 @@ export function normalizeOtpauth (input, label = 'dotrino') {
38
38
  /** Parsea `otpauth://totp/Emisor:cuenta?secret=...&digits=6&period=30&algorithm=SHA1`. */
39
39
  export function parseOtpauth (uri) {
40
40
  const u = new URL(normalizeOtpauth(uri))
41
- if (u.protocol !== 'otpauth:') throw new Error('otpauth: esquema inválido')
41
+ if (u.protocol !== 'otpauth:') throw new Error('otpauth: invalid scheme')
42
42
  const type = u.hostname.toLowerCase()
43
- if (type !== 'totp' && type !== 'hotp') throw new Error('otpauth: tipo no soportado')
43
+ if (type !== 'totp' && type !== 'hotp') throw new Error('otpauth: unsupported type')
44
44
  const q = u.searchParams
45
45
  const secret = q.get('secret')
46
- if (!secret) throw new Error('otpauth: falta secret')
46
+ if (!secret) throw new Error('otpauth: missing secret')
47
47
 
48
48
  const label = decodeURIComponent(u.pathname.replace(/^\//, ''))
49
49
  const [maybeIssuer, account] = label.includes(':') ? label.split(/:(.+)/) : [null, label]
@@ -74,7 +74,7 @@ const HASHES = { SHA1: 'SHA-1', SHA256: 'SHA-256', SHA512: 'SHA-512' }
74
74
 
75
75
  async function hotp (secretBytes, counter, digits, algorithm) {
76
76
  const hash = HASHES[algorithm]
77
- if (!hash) throw new Error('otp: algoritmo no soportado: ' + algorithm)
77
+ if (!hash) throw new Error('otp: unsupported algorithm: ' + algorithm)
78
78
 
79
79
  const buf = new ArrayBuffer(8)
80
80
  const view = new DataView(buf)
@@ -77,7 +77,7 @@ export class ProxyTransport {
77
77
  this.pending.delete(rid)
78
78
  // No es "no hay nadie": es que nadie contestó. Suele ser el teléfono sin
79
79
  // que nadie lo mire, y el código lo distingue para poder decirlo bien.
80
- reject(new VaultError(CODES.APPROVAL_TIMEOUT, 'nadie respondió a tiempo'))
80
+ reject(new VaultError(CODES.APPROVAL_TIMEOUT, 'nobody replied in time'))
81
81
  }, this.timeoutMs)
82
82
 
83
83
  this.pending.set(rid, { resolve, reject, timer })
@@ -94,7 +94,7 @@ export class ProxyTransport {
94
94
  // «no tengo la llave del otro lado» y «se cayó la red» se vieran igual desde
95
95
  // arriba, y son cosas distintas: una se arregla enlazando, la otra esperando.
96
96
  if (e instanceof VaultError) return reject(e)
97
- reject(new VaultError(e?.code || CODES.UNREACHABLE, e?.message || 'no se pudo enviar'))
97
+ reject(new VaultError(e?.code || CODES.UNREACHABLE, e?.message || 'could not send'))
98
98
  })
99
99
  })
100
100
  }
@@ -103,7 +103,7 @@ export class ProxyTransport {
103
103
  close (code = CODES.UNREACHABLE) {
104
104
  for (const [rid, p] of this.pending) {
105
105
  clearTimeout(p.timer)
106
- p.reject(new VaultError(code, 'transporte cerrado'))
106
+ p.reject(new VaultError(code, 'transport closed'))
107
107
  this.pending.delete(rid)
108
108
  }
109
109
  }
@@ -64,7 +64,7 @@ export class GuardedVault {
64
64
 
65
65
  if (pedidasPrivadas.length) {
66
66
  const ok = await this.gate.allow({ op: 'get', payload: { id, keys: pedidasPrivadas } })
67
- if (!ok) throw new VaultError(CODES.NOT_APPROVED, 'no aprobado')
67
+ if (!ok) throw new VaultError(CODES.NOT_APPROVED, 'not approved')
68
68
  }
69
69
  return projectEntry(open, keys)
70
70
  }
@@ -38,7 +38,7 @@ export class LocalVault {
38
38
  }
39
39
 
40
40
  #requireKey () {
41
- if (!this.key) throw new VaultError(CODES.LOCKED, 'la bóveda está cerrada')
41
+ if (!this.key) throw new VaultError(CODES.LOCKED, 'the vault is locked')
42
42
  }
43
43
 
44
44
  async #all () {
@@ -73,13 +73,13 @@ export class LocalVault {
73
73
  async get (id) {
74
74
  this.#requireKey()
75
75
  const entry = (await this.#all()).find(e => e.id === id)
76
- if (!entry) throw new VaultError(CODES.NOT_FOUND, 'no hay ninguna entrada con ese id')
76
+ if (!entry) throw new VaultError(CODES.NOT_FOUND, 'there is no entry with that id')
77
77
  return openEntry(this.key, entry)
78
78
  }
79
79
 
80
80
  async put (plain) {
81
81
  this.#requireKey()
82
- if (this.readOnly) throw new VaultError(CODES.READ_ONLY, 'esta copia solo puede leer')
82
+ if (this.readOnly) throw new VaultError(CODES.READ_ONLY, 'this copy is read-only')
83
83
  const sealed = await sealEntry(this.key, plain)
84
84
  const all = await this.#all()
85
85
  const i = all.findIndex(e => e.id === sealed.id)
@@ -122,10 +122,10 @@ export class LocalVault {
122
122
  */
123
123
  async patch (id, changes = {}) {
124
124
  this.#requireKey()
125
- if (this.readOnly) throw new VaultError(CODES.READ_ONLY, 'esta copia solo puede leer')
125
+ if (this.readOnly) throw new VaultError(CODES.READ_ONLY, 'this copy is read-only')
126
126
  const all = await this.#all()
127
127
  const i = all.findIndex(e => e.id === id)
128
- if (i < 0) throw new VaultError(CODES.NOT_FOUND, 'no hay ninguna entrada con ese id')
128
+ if (i < 0) throw new VaultError(CODES.NOT_FOUND, 'there is no entry with that id')
129
129
 
130
130
  const base = await openEntry(this.key, all[i])
131
131
  // La fusión vive en `model.js` y la comparten esta bóveda y la sellada: no es un
@@ -199,10 +199,10 @@ export class LocalVault {
199
199
 
200
200
  async remove (id) {
201
201
  this.#requireKey()
202
- if (this.readOnly) throw new VaultError(CODES.READ_ONLY, 'esta copia solo puede leer')
202
+ if (this.readOnly) throw new VaultError(CODES.READ_ONLY, 'this copy is read-only')
203
203
  const all = await this.#all()
204
204
  const rest = all.filter(e => e.id !== id)
205
- if (rest.length === all.length) throw new VaultError(CODES.NOT_FOUND, 'no hay ninguna entrada con ese id')
205
+ if (rest.length === all.length) throw new VaultError(CODES.NOT_FOUND, 'there is no entry with that id')
206
206
  await this.store.set(KEY, rest)
207
207
  }
208
208
 
@@ -70,7 +70,7 @@ export class RemoteVault {
70
70
  }
71
71
 
72
72
  async list () {
73
- throw new VaultError(CODES.NO_KEY, 'una bóveda remota no lista: pide de a una')
73
+ throw new VaultError(CODES.NO_KEY, 'a remote vault does not list: ask one at a time')
74
74
  }
75
75
 
76
76
  async #request (op, payload) {
@@ -79,7 +79,7 @@ export class RemoteVault {
79
79
  } catch (e) {
80
80
  // El transporte puede fallar de muchas formas; hacia arriba solo hay códigos.
81
81
  if (e instanceof VaultError) throw e
82
- throw new VaultError(e?.code || CODES.UNREACHABLE, e?.message || 'no hay bóveda al otro lado')
82
+ throw new VaultError(e?.code || CODES.UNREACHABLE, e?.message || 'no vault on the other side')
83
83
  }
84
84
  }
85
85
  }
@@ -124,7 +124,7 @@ export class VaultResponder {
124
124
  // 1. ¿Es un aparato que conozco? Si no, ni se le contesta qué falló.
125
125
  if (!fromPubkey || !this.isAllowed(fromPubkey)) {
126
126
  this.onRequest({ ...record, outcome: 'denied' })
127
- return this.#send(fromPubkey, replyError(msg.rid, CODES.DENIED, 'aparato no autorizado'))
127
+ return this.#send(fromPubkey, replyError(msg.rid, CODES.DENIED, 'device not authorized'))
128
128
  }
129
129
 
130
130
  // 2. `list` no está en la lista, y esa ausencia es el diseño: si un aparato
@@ -132,11 +132,11 @@ export class VaultResponder {
132
132
  const esAdmin = ADMIN_OPS.includes(msg.op)
133
133
  if (!REMOTE_OPS.includes(msg.op) && !esAdmin) {
134
134
  this.onRequest({ ...record, outcome: 'denied' })
135
- return this.#send(fromPubkey, replyError(msg.rid, CODES.DENIED, 'operación no permitida en remoto'))
135
+ return this.#send(fromPubkey, replyError(msg.rid, CODES.DENIED, 'operation not allowed remotely'))
136
136
  }
137
137
  if (esAdmin && !this.admin) {
138
138
  this.onRequest({ ...record, outcome: 'denied' })
139
- return this.#send(fromPubkey, replyError(msg.rid, CODES.DENIED, 'esta bóveda no se administra en remoto'))
139
+ return this.#send(fromPubkey, replyError(msg.rid, CODES.DENIED, 'this vault is not managed remotely'))
140
140
  }
141
141
 
142
142
  try {
@@ -149,7 +149,7 @@ export class VaultResponder {
149
149
  const ok = await this.approve({ op: msg.op, payload, pubkey: fromPubkey, admin: true })
150
150
  if (!ok) {
151
151
  this.onRequest({ ...record, outcome: 'refused' })
152
- return this.#send(fromPubkey, replyError(msg.rid, CODES.NOT_APPROVED, 'no aprobado'))
152
+ return this.#send(fromPubkey, replyError(msg.rid, CODES.NOT_APPROVED, 'not approved'))
153
153
  }
154
154
  const result = msg.op === 'devices'
155
155
  ? await this.admin.devices()
@@ -162,7 +162,7 @@ export class VaultResponder {
162
162
  const ok = await this.gate.allow({ op: msg.op, payload, pubkey: fromPubkey })
163
163
  if (!ok) {
164
164
  this.onRequest({ ...record, outcome: 'refused' })
165
- return this.#send(fromPubkey, replyError(msg.rid, CODES.NOT_APPROVED, 'no aprobado'))
165
+ return this.#send(fromPubkey, replyError(msg.rid, CODES.NOT_APPROVED, 'not approved'))
166
166
  }
167
167
  }
168
168
 
@@ -144,10 +144,10 @@ export class SealedLocalVault {
144
144
  let got
145
145
  try { got = await this.sealed.get(id, { pub: this.readerPub, keys }) }
146
146
  catch (e) {
147
- if (e?.code === 'not-found') throw new VaultError(CODES.NOT_FOUND, 'no hay ninguna entrada con ese id')
147
+ if (e?.code === 'not-found') throw new VaultError(CODES.NOT_FOUND, 'there is no entry with that id')
148
148
  // «no me la envolvieron a mí» no es «no existe», y confundirlas manda a buscar el
149
149
  // problema al sitio equivocado.
150
- if (e?.code === 'not-yours') throw new VaultError(CODES.NOT_ALLOWED, 'esta punta no tiene llave para esa entrada')
150
+ if (e?.code === 'not-yours') throw new VaultError(CODES.NOT_ALLOWED, 'this end has no key for that entry')
151
151
  throw e
152
152
  }
153
153
  return openSealedEntry({ got, openSealed: this.openSealed })
@@ -64,12 +64,12 @@ export class SealedVault {
64
64
  forget () { this._keys = null; this._recipients = null }
65
65
 
66
66
  async #request (op, payload) {
67
- if (!SEALED_OPS.includes(op)) throw new VaultError(CODES.NOT_ALLOWED, 'operación desconocida: ' + op)
67
+ if (!SEALED_OPS.includes(op)) throw new VaultError(CODES.NOT_ALLOWED, 'unknown operation: ' + op)
68
68
  try {
69
69
  return await this.transport.request(op, payload)
70
70
  } catch (e) {
71
71
  if (e instanceof VaultError) throw e
72
- throw new VaultError(e?.code || CODES.UNREACHABLE, e?.message || 'no hay bóveda al otro lado')
72
+ throw new VaultError(e?.code || CODES.UNREACHABLE, e?.message || 'no vault on the other side')
73
73
  }
74
74
  }
75
75
 
@@ -194,7 +194,7 @@ export class SealedVault {
194
194
  async patch (id, changes = {}) {
195
195
  const [keys, recipients] = await Promise.all([this.keys(), this.recipients()])
196
196
  const actual = await this.#viewById(id)
197
- if (!actual) throw new VaultError(CODES.NOT_FOUND, 'no hay ninguna entrada con ese id')
197
+ if (!actual) throw new VaultError(CODES.NOT_FOUND, 'there is no entry with that id')
198
198
 
199
199
  const values = {}
200
200
  const drop = []
@@ -1,4 +1,4 @@
1
- Copia vendorizada de @dotrino/proxy-client@0.36.2 (dotrino-proxy-client/src/{binpack,canonical,client,encpub,errors,index,parts,sealing,signature,stats-log,stats,webrtc}.js).
1
+ Copia vendorizada de @dotrino/proxy-client@0.38.2 (dotrino-proxy-client/src/{binpack,canonical,client,encpub,errors,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.
@@ -270,9 +270,9 @@ export class WebSocketProxyClient {
270
270
 
271
271
  /** Corta en seco todo lo que esperaba respuesta, con su motivo. */
272
272
  _rejectAllPending (error) {
273
- const pendientes = [...this._pending.values()]
273
+ const pending = [...this._pending.values()]
274
274
  this._pending.clear()
275
- for (const entry of pendientes) {
275
+ for (const entry of pending) {
276
276
  clearTimeout(entry.timer)
277
277
  entry.reject(error)
278
278
  }
@@ -432,7 +432,11 @@ export class WebSocketProxyClient {
432
432
  async list (channelName) {
433
433
  const channel = await buildSignedChannel(channelName)
434
434
  const res = await this._request({ type: 'list', channel }, 'channel_list', 'channel')
435
- return res.tokens || []
435
+ const tokens = res.tokens || []
436
+ // Vacío pero con una bóveda despertándose detrás (websocket-proxy ≥ 1.7.0): quien lista
437
+ // puede esperar unos segundos en vez de concluir que no hay nadie.
438
+ if (res.waking) tokens.waking = true
439
+ return tokens
436
440
  }
437
441
 
438
442
  /** Alias for list() to ease migration from older clients. */
@@ -533,8 +537,8 @@ export class WebSocketProxyClient {
533
537
  // Y SE RESUELVEN TODAS ANTES DE MANDAR NADA. Enviar a los que se pudo y fallar por el
534
538
  // resto deja a la app creyendo que el mensaje salió, con la mitad de la sala sin él y
535
539
  // sin forma de saber cuál mitad.
536
- const llaves = await Promise.all(list.map(async (pk) => [pk, await this.encPubOf(pk)]))
537
- for (const [pk, encPub] of llaves) {
540
+ const keys = await Promise.all(list.map(async (pk) => [pk, await this.encPubOf(pk)]))
541
+ for (const [pk, encPub] of keys) {
538
542
  const sealPiece = await this._sealerFor(m.bin, encPub, pk)
539
543
  for (const piece of pieces) this._sendByPubkeyRaw([pk], await sealPiece(piece), opts)
540
544
  }
@@ -582,10 +586,10 @@ export class WebSocketProxyClient {
582
586
  if (m.bin && !direct) this._needBinaryProxy()
583
587
  const sealPiece = await this._sealerFor(m.bin, peerEncPub, peerPubkey)
584
588
  if (m.len <= size) {
585
- const sobre = await sealPiece(m.bin ? m.whole : payload)
586
- if (tokens.length === 1 && peerPubkey) { this._sendToTokenOrQueue(tokens[0], sobre, peerPubkey); return }
589
+ const envelope = await sealPiece(m.bin ? m.whole : payload)
590
+ if (tokens.length === 1 && peerPubkey) { this._sendToTokenOrQueue(tokens[0], envelope, peerPubkey); return }
587
591
  // Por `send`, no por `_sendRaw`: así sigue prefiriendo el canal directo si lo hay.
588
- this.send(tokens, sobre)
592
+ this.send(tokens, envelope)
589
593
  return
590
594
  }
591
595
  // Por el proxio en vivo hay tope (parts.js): lo que pasa de él solo sale por canal
@@ -601,9 +605,9 @@ export class WebSocketProxyClient {
601
605
  // mensaje entero cabe por ahí; si no, se para y se dice (`direct-lost`).
602
606
  const relayFallback = m.len <= relayMax
603
607
  for (const piece of (m.bin ? splitBinParts(m.whole, size) : splitParts(m.whole, size))) {
604
- const sobre = await sealPiece(piece)
605
- if (tokens.length === 1 && peerPubkey) await this._sendPiece(tokens[0], sobre, peerPubkey, queueFallback, relayFallback)
606
- else this.send(tokens, sobre)
608
+ const envelope = await sealPiece(piece)
609
+ if (tokens.length === 1 && peerPubkey) await this._sendPiece(tokens[0], envelope, peerPubkey, queueFallback, relayFallback)
610
+ else this.send(tokens, envelope)
607
611
  }
608
612
  }
609
613
 
@@ -632,13 +636,13 @@ export class WebSocketProxyClient {
632
636
  * archivo grande lo desborda y el navegador cierra el canal), o por el proxio en vivo con
633
637
  * una pausa corta entre partes, para no chocar con su límite de mensajes por segundo.
634
638
  */
635
- async _sendPiece (token, sobre, peerPubkey, queueFallback, relayFallback = true) {
636
- if (this._rtc && await this._rtc.sendDirect(token, this._wire(sobre))) return
639
+ async _sendPiece (token, envelope, peerPubkey, queueFallback, relayFallback = true) {
640
+ if (this._rtc && await this._rtc.sendDirect(token, this._wire(envelope))) return
637
641
  if (!relayFallback) {
638
642
  throw errorWithCode('the direct channel closed in the middle of a message too big for the proxy', 'direct-lost')
639
643
  }
640
- if (sobre instanceof Uint8Array) this._needBinaryProxy()
641
- this._sendToTokenOrQueue(token, sobre, peerPubkey, { queueFallback, skipDirect: true })
644
+ if (envelope instanceof Uint8Array) this._needBinaryProxy()
645
+ this._sendToTokenOrQueue(token, envelope, peerPubkey, { queueFallback, skipDirect: true })
642
646
  await new Promise((r) => setTimeout(r, 20))
643
647
  }
644
648
 
@@ -674,16 +678,16 @@ export class WebSocketProxyClient {
674
678
  * `token_gone` para que la app deje de usar ese token. No bloquea a nadie: se manda ya y
675
679
  * el arreglo, si hace falta, llega detrás.
676
680
  */
677
- _sendToTokenOrQueue (token, sobre, peerPubkey, { queueFallback = true, skipDirect = false } = {}) {
678
- if (this.requireSealed && !this._isSealed(sobre)) {
681
+ _sendToTokenOrQueue (token, envelope, peerPubkey, { queueFallback = true, skipDirect = false } = {}) {
682
+ if (this.requireSealed && !this._isSealed(envelope)) {
679
683
  throw errorWithCode('requireSealed: refusing to send a directed message in the clear — use sendSealedTo()', 'unsealed')
680
684
  }
681
- const messageStr = this._wire(sobre)
685
+ const messageStr = this._wire(envelope)
682
686
  if (!skipDirect && this._rtc && this._rtc.trySend(token, messageStr)) return
683
687
  const id = `msg_${this._nextId++}`
684
688
  const timer = setTimeout(() => this._tokenWatch.delete(id), 15000)
685
689
  timer.unref?.()
686
- this._tokenWatch.set(id, { token, sobre, peerPubkey, timer, queueFallback })
690
+ this._tokenWatch.set(id, { token, envelope, peerPubkey, timer, queueFallback })
687
691
  try {
688
692
  this._sendRaw({ to: [token], message: messageStr, id })
689
693
  } catch (e) {
@@ -707,7 +711,7 @@ export class WebSocketProxyClient {
707
711
  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 })
708
712
  return
709
713
  }
710
- try { this._sendByPubkeyRaw([w.peerPubkey], w.sobre) } catch (e) { this._emit('error', { type: 'client', error: e.message, code: e.code || null }) }
714
+ try { this._sendByPubkeyRaw([w.peerPubkey], w.envelope) } catch (e) { this._emit('error', { type: 'client', error: e.message, code: e.code || null }) }
711
715
  }
712
716
 
713
717
  // ---------- el saludo: de quién es este token ----------
@@ -896,16 +900,16 @@ export class WebSocketProxyClient {
896
900
  if (typeof publickey !== 'string' || !publickey) {
897
901
  throw errorWithCode('encPubOf: missing publickey', 'encpub-shape')
898
902
  }
899
- const cacheada = this._encPubs.get(publickey)
900
- if (cacheada) return cacheada
903
+ const cached = this._encPubs.get(publickey)
904
+ if (cached) return cached
901
905
  // Una pregunta en vuelo por llave: una sala de ocho manda ocho mensajes a la vez y
902
906
  // preguntaría ocho veces por lo mismo.
903
- const enVuelo = this._encPubInflight.get(publickey)
904
- if (enVuelo) return enVuelo
905
- const promesa = this._lookupEncPub(publickey)
907
+ const inflight = this._encPubInflight.get(publickey)
908
+ if (inflight) return inflight
909
+ const promise = this._lookupEncPub(publickey)
906
910
  .finally(() => this._encPubInflight.delete(publickey))
907
- this._encPubInflight.set(publickey, promesa)
908
- return promesa
911
+ this._encPubInflight.set(publickey, promise)
912
+ return promise
909
913
  }
910
914
 
911
915
  async _lookupEncPub (publickey) {
@@ -913,13 +917,13 @@ export class WebSocketProxyClient {
913
917
  // ya la tiene en la mano y firmada por el master: preguntársela a un servidor sería
914
918
  // dar la vuelta para llegar a lo que ya está aquí.
915
919
  if (this.encPubResolver) {
916
- const propia = await this.encPubResolver(publickey)
917
- if (propia) {
918
- if (!isEncPub(propia)) {
920
+ const own = await this.encPubResolver(publickey)
921
+ if (own) {
922
+ if (!isEncPub(own)) {
919
923
  throw errorWithCode('encPubResolver returned something that is not a P-256 public JWK', 'encpub-unverified')
920
924
  }
921
- this._encPubs.set(publickey, propia)
922
- return propia
925
+ this._encPubs.set(publickey, own)
926
+ return own
923
927
  }
924
928
  }
925
929
  // SI EL PROXIO DICE QUE NO SABE DE ESTO, se dice con su propio código en vez de
@@ -927,7 +931,7 @@ export class WebSocketProxyClient {
927
931
  // que esto existiera y no se sabe: se pregunta igual, y contesta la pregunta.
928
932
  if (Array.isArray(this.caps) && !this.caps.includes('encpub')) {
929
933
  throw errorWithCode(
930
- `este proxio (${this.url}) no sirve llaves de cifrado: hace falta websocket-proxy >= 1.1.0`,
934
+ `this proxy (${this.url}) does not serve encryption keys: it needs websocket-proxy >= 1.1.0`,
931
935
  'no-encpub-support')
932
936
  }
933
937
  const res = await this._request({ type: 'enc-lookup', publickeys: [publickey] }, 'enc-lookup')
@@ -1145,8 +1149,8 @@ export class WebSocketProxyClient {
1145
1149
  // conexión, y por eso este sobre no necesita otro. Lo que le faltaba era decir a quién
1146
1150
  // se lo estamos dando.
1147
1151
  const data = { op: 'identify', aud: this.audience, publickey, token: this.token, ts: Date.now() }
1148
- const firmado = await sign(data)
1149
- const signature = typeof firmado === 'string' ? firmado : firmado?.signature
1152
+ const signed = await sign(data)
1153
+ const signature = typeof signed === 'string' ? signed : signed?.signature
1150
1154
  // «No pude firmar» y «se cayó la red» son cosas distintas, y se distinguen por el
1151
1155
  // `code`: la bóveda usa esto para saber si su llave de comunicación firma todavía, y
1152
1156
  // tragarse un fallo de red como si fuera lo primero la mandaría al camino equivocado.
@@ -1229,7 +1233,7 @@ export class WebSocketProxyClient {
1229
1233
  * @returns {Promise<boolean>} true si TURN quedó activo.
1230
1234
  */
1231
1235
  async enableTurn ({ publicKey, sign } = {}) {
1232
- if (!this._rtc) throw new Error('WebRTC está deshabilitado en este cliente')
1236
+ if (!this._rtc) throw new Error('WebRTC is disabled in this client')
1233
1237
  if (this._turnTimer) {
1234
1238
  clearTimeout(this._turnTimer)
1235
1239
  this._turnTimer = null
@@ -1316,17 +1320,17 @@ export class WebSocketProxyClient {
1316
1320
  */
1317
1321
  async enablePush ({ publicKey, sign, vapidPublicKey, registration, swPath, swScope } = {}) {
1318
1322
  if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
1319
- throw new Error('Service Worker no soportado en este entorno')
1323
+ throw new Error('Service Worker is not supported in this environment')
1320
1324
  }
1321
1325
  if (typeof PushManager === 'undefined') {
1322
- throw new Error('Push API no soportada en este navegador')
1326
+ throw new Error('the Push API is not supported in this browser')
1323
1327
  }
1324
1328
  if (!publicKey || typeof sign !== 'function') {
1325
1329
  throw new Error('enablePush requires { publicKey, sign }')
1326
1330
  }
1327
1331
  if (!vapidPublicKey) {
1328
1332
  const cfg = await this.getPushConfig()
1329
- if (!cfg.enabled || !cfg.vapidPublicKey) throw new Error('El proxy no tiene Web Push habilitado')
1333
+ if (!cfg.enabled || !cfg.vapidPublicKey) throw new Error('the proxy does not have Web Push enabled')
1330
1334
  vapidPublicKey = cfg.vapidPublicKey
1331
1335
  }
1332
1336
  let reg
@@ -1367,6 +1371,29 @@ export class WebSocketProxyClient {
1367
1371
  return { kind, token }
1368
1372
  }
1369
1373
 
1374
+ /**
1375
+ * DESPIÉRTAME AQUÍ: el timbre de una bóveda que no vive conectada (la de una Lambda). Si
1376
+ * llega algo para la PERSONA de este acta y no hay nadie suyo conectado, el proxio guarda el
1377
+ * mensaje en la cola de siempre y hace un POST a `url`; la bóveda arranca, se identifica y
1378
+ * recibe lo encolado.
1379
+ *
1380
+ * Lo firma la llave que habla por la bóveda (su llave de comunicación), no la maestra, y el
1381
+ * `acta` es lo que prueba que esa llave es de la cuenta: el timbre se guarda para la cuenta
1382
+ * (`profileId`), que es a quien le escriben los servicios.
1383
+ *
1384
+ * `channel` (opcional): el canal donde la buscan quienes no saben su llave —quien entra con
1385
+ * usuario y contraseña—. Listarlo vacío también la despierta (`list()` devuelve `waking`).
1386
+ *
1387
+ * @param {{ publicKey:string, sign:Function, url:string, acta:object, channel?:string }} opts
1388
+ */
1389
+ async registerWake ({ publicKey, sign, url, acta, channel } = {}) {
1390
+ if (!publicKey || typeof sign !== 'function' || !url || !acta) throw errorWithCode('registerWake requires { publicKey, sign, url, acta }', 'bad-args')
1391
+ const data = { op: 'push-subscribe', publickey: publicKey, subscription: JSON.stringify({ kind: 'wake', url, ...(channel ? { channel } : {}) }), ts: Date.now() }
1392
+ const signature = await normalizeSignature(sign, data)
1393
+ const r = await this._request({ type: 'push-subscribe', data, signature, acta }, 'push-subscribed')
1394
+ return { kind: 'wake', url, profile: r?.publickey || null }
1395
+ }
1396
+
1370
1397
  /**
1371
1398
  * Desactivar Web Push: cancela la PushSubscription local y la borra del proxy.
1372
1399
  * @param {Object} opts
@@ -1540,8 +1567,8 @@ export class WebSocketProxyClient {
1540
1567
  // `_reconnectAttempts` vuelve a 0 al abrir, así que esto NO convierte un `connect()`
1541
1568
  // inicial fallido en un bucle de fondo: ahí vale 0 y la promesa se rechaza como
1542
1569
  // siempre.
1543
- const reintentando = this._reconnectAttempts > 0
1544
- if ((wasConnected || reintentando) && this.autoReconnect) {
1570
+ const retrying = this._reconnectAttempts > 0
1571
+ if ((wasConnected || retrying) && this.autoReconnect) {
1545
1572
  this._scheduleReconnect()
1546
1573
  }
1547
1574
  })
@@ -1776,14 +1803,14 @@ export class WebSocketProxyClient {
1776
1803
  this._countOut(frame, bytes)
1777
1804
  return
1778
1805
  }
1779
- const texto = JSON.stringify(frame)
1780
- this.ws.send(texto)
1781
- this._countOut(frame, texto)
1806
+ const text = JSON.stringify(frame)
1807
+ this.ws.send(text)
1808
+ this._countOut(frame, text)
1782
1809
  }
1783
1810
 
1784
1811
  /** Cuenta un frame que sale por el proxio, y a quién iba. */
1785
- _countOut (frame, texto) {
1786
- this._traffic.frame('out', utf8Length(texto))
1812
+ _countOut (frame, text) {
1813
+ this._traffic.frame('out', utf8Length(text))
1787
1814
  if (typeof frame.message !== 'string' && !(frame.message instanceof Uint8Array)) return
1788
1815
  const bytes = utf8Length(frame.message)
1789
1816
  for (const token of Array.isArray(frame.to) ? frame.to : []) {
@@ -1897,7 +1924,7 @@ export class WebSocketProxyClient {
1897
1924
  async function normalizeSignature (sign, data) {
1898
1925
  const out = await sign(data)
1899
1926
  const sig = typeof out === 'string' ? out : (out && out.signature)
1900
- if (!sig) throw new Error('sign() debe devolver una firma base64 (string o {signature})')
1927
+ if (!sig) throw new Error('sign() must return a base64 signature (a string or {signature})')
1901
1928
  return sig
1902
1929
  }
1903
1930
 
@@ -67,8 +67,8 @@ export async function buildEncPubStatement ({ publickey, encPub, sign } = /** @t
67
67
  if (!isEncPub(encPub)) throw errorWithCode('buildEncPubStatement: encPub is not a P-256 public JWK', 'encpub-shape')
68
68
  if (typeof sign !== 'function') throw errorWithCode('buildEncPubStatement: missing sign(data)', 'encpub-shape')
69
69
  const data = encPubBody({ publickey, encPub })
70
- const firmado = await sign(data)
71
- const signature = typeof firmado === 'string' ? firmado : firmado?.signature
70
+ const signed = await sign(data)
71
+ const signature = typeof signed === 'string' ? signed : signed?.signature
72
72
  // «No pude firmar» no es «se cayó la red», y aquí es donde se separan: quien llama
73
73
  // decide una cosa u otra según el `code`, nunca según la frase.
74
74
  if (typeof signature !== 'string') throw errorWithCode('buildEncPubStatement: sign() returned no signature', 'no-signature')