@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 +1 -1
- package/vault/remote.js +17 -2
- package/vault/vendor/passmanager/VERSION.txt +1 -1
- package/vault/vendor/passmanager/generate.js +1 -1
- package/vault/vendor/passmanager/import.js +3 -3
- package/vault/vendor/passmanager/totp.js +5 -5
- package/vault/vendor/passmanager/transport/proxy.js +3 -3
- package/vault/vendor/passmanager/vault/guard.js +1 -1
- package/vault/vendor/passmanager/vault/local.js +7 -7
- package/vault/vendor/passmanager/vault/remote.js +2 -2
- package/vault/vendor/passmanager/vault/responder.js +5 -5
- package/vault/vendor/passmanager/vault/sealed-local.js +2 -2
- package/vault/vendor/passmanager/vault/sealed.js +3 -3
- package/vault/vendor/proxy-client/VERSION.txt +1 -1
- package/vault/vendor/proxy-client/client.js +74 -47
- package/vault/vendor/proxy-client/encpub.js +2 -2
- package/vault/vendor/proxy-client/stats.js +7 -7
- package/vault/vendor/proxy-client/webrtc.js +10 -10
- package/vault/vendor/roadmap/VERSION.txt +1 -1
- package/vault/vendor/roadmap/index.js +4 -4
- package/vault/vendor/roadmap/manifests.js +46 -25
- package/vault/vendor/vault/VERSION.txt +1 -1
- package/vault/vendor/vault/core/libVersion.js +1 -1
- package/vault/vendor/vault/core/secretsStore.js +2 -2
- package/vault/vendor/vault/core/transport.js +21 -1
- package/vault/vendor/vault/core/vault.js +9 -2
- package/vault/vendor/vault/enroll.js +75 -27
- package/vault/vendor/vault/loginClient.js +10 -1
- package/vault/vendor/vault/passwordLogins.js +25 -2
package/package.json
CHANGED
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.
|
|
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:
|
|
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('
|
|
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: '
|
|
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('
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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, '
|
|
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 || '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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
|
|
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, '
|
|
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.
|
|
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
|
|
273
|
+
const pending = [...this._pending.values()]
|
|
274
274
|
this._pending.clear()
|
|
275
|
-
for (const entry of
|
|
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
|
-
|
|
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
|
|
537
|
-
for (const [pk, encPub] of
|
|
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
|
|
586
|
-
if (tokens.length === 1 && peerPubkey) { this._sendToTokenOrQueue(tokens[0],
|
|
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,
|
|
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
|
|
605
|
-
if (tokens.length === 1 && peerPubkey) await this._sendPiece(tokens[0],
|
|
606
|
-
else this.send(tokens,
|
|
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,
|
|
636
|
-
if (this._rtc && await this._rtc.sendDirect(token, this._wire(
|
|
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 (
|
|
641
|
-
this._sendToTokenOrQueue(token,
|
|
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,
|
|
678
|
-
if (this.requireSealed && !this._isSealed(
|
|
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(
|
|
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,
|
|
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.
|
|
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
|
|
900
|
-
if (
|
|
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
|
|
904
|
-
if (
|
|
905
|
-
const
|
|
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,
|
|
908
|
-
return
|
|
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
|
|
917
|
-
if (
|
|
918
|
-
if (!isEncPub(
|
|
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,
|
|
922
|
-
return
|
|
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
|
-
`
|
|
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
|
|
1149
|
-
const signature = typeof
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
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
|
|
1544
|
-
if ((wasConnected ||
|
|
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
|
|
1780
|
-
this.ws.send(
|
|
1781
|
-
this._countOut(frame,
|
|
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,
|
|
1786
|
-
this._traffic.frame('out', utf8Length(
|
|
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()
|
|
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
|
|
71
|
-
const signature = typeof
|
|
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')
|