@dotrino/identity 0.121.0 → 0.121.1
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/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/loginClient.js +10 -1
- package/vault/vendor/vault/passwordLogins.js +25 -2
package/package.json
CHANGED
|
@@ -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')
|
|
@@ -37,7 +37,7 @@ export function utf8Length (s) {
|
|
|
37
37
|
|
|
38
38
|
export const PATHS = ['proxy', 'direct', 'turn', 'webrtc']
|
|
39
39
|
|
|
40
|
-
const
|
|
40
|
+
const zeros = () => ({ proxy: 0, direct: 0, turn: 0, webrtc: 0 })
|
|
41
41
|
|
|
42
42
|
export class TrafficStats {
|
|
43
43
|
constructor () {
|
|
@@ -64,7 +64,7 @@ export class TrafficStats {
|
|
|
64
64
|
if (!key) return
|
|
65
65
|
let p = this.peers.get(key)
|
|
66
66
|
if (!p) {
|
|
67
|
-
p = { token: who.token || null, pubkey: who.pubkey || null, bytesIn:
|
|
67
|
+
p = { token: who.token || null, pubkey: who.pubkey || null, bytesIn: zeros(), bytesOut: zeros(), msgsIn: 0, msgsOut: 0, firstAt: Date.now(), lastAt: 0 }
|
|
68
68
|
this.peers.set(key, p)
|
|
69
69
|
}
|
|
70
70
|
if (who.pubkey && !p.pubkey) p.pubkey = who.pubkey
|
|
@@ -104,8 +104,8 @@ export async function routeOf (pc) {
|
|
|
104
104
|
const local = byId.get(pair.localCandidateId)
|
|
105
105
|
const remote = byId.get(pair.remoteCandidateId)
|
|
106
106
|
if (!local && !remote) return null
|
|
107
|
-
const
|
|
108
|
-
return
|
|
107
|
+
const typeOf = (c) => c && (c.candidateType || c.type)
|
|
108
|
+
return typeOf(local) === 'relay' || typeOf(remote) === 'relay' ? 'turn' : 'direct'
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
@@ -121,7 +121,7 @@ function registry () {
|
|
|
121
121
|
return globalThis[REGISTRY]
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
function
|
|
124
|
+
function notify () {
|
|
125
125
|
if (typeof globalThis.dispatchEvent === 'function' && typeof Event === 'function') {
|
|
126
126
|
try { globalThis.dispatchEvent(new Event('dotrino-transports')) } catch (_) {}
|
|
127
127
|
}
|
|
@@ -131,11 +131,11 @@ export function registerTransport (client) {
|
|
|
131
131
|
const r = registry()
|
|
132
132
|
if (r.has(client)) return
|
|
133
133
|
r.add(client)
|
|
134
|
-
|
|
134
|
+
notify()
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
export function unregisterTransport (client) {
|
|
138
|
-
if (registry().delete(client))
|
|
138
|
+
if (registry().delete(client)) notify()
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/** Los clientes registrados en esta página (o proceso). */
|
|
@@ -48,13 +48,13 @@ const RTC_TAG = '__cc_rtc__'
|
|
|
48
48
|
* es el escalón 4, que siempre funciona.
|
|
49
49
|
*/
|
|
50
50
|
let _PC = null
|
|
51
|
-
let
|
|
51
|
+
let _PCLookedUp = false
|
|
52
52
|
|
|
53
|
-
export function setPeerConnection (impl) { _PC = impl;
|
|
53
|
+
export function setPeerConnection (impl) { _PC = impl; _PCLookedUp = true }
|
|
54
54
|
|
|
55
55
|
export function resolvePeerConnection () {
|
|
56
|
-
if (
|
|
57
|
-
|
|
56
|
+
if (_PCLookedUp) return _PC
|
|
57
|
+
_PCLookedUp = true
|
|
58
58
|
if (typeof globalThis.RTCPeerConnection === 'function') { _PC = globalThis.RTCPeerConnection; return _PC }
|
|
59
59
|
return _PC
|
|
60
60
|
}
|
|
@@ -64,15 +64,15 @@ export function resolvePeerConnection () {
|
|
|
64
64
|
* llama una vez al arrancar, no en medio de una negociación.
|
|
65
65
|
*/
|
|
66
66
|
export async function loadNodePeerConnection () {
|
|
67
|
-
if (
|
|
68
|
-
if (typeof globalThis.RTCPeerConnection === 'function') { _PC = globalThis.RTCPeerConnection;
|
|
69
|
-
for (const
|
|
67
|
+
if (_PCLookedUp && _PC) return _PC
|
|
68
|
+
if (typeof globalThis.RTCPeerConnection === 'function') { _PC = globalThis.RTCPeerConnection; _PCLookedUp = true; return _PC }
|
|
69
|
+
for (const name of ['@dotrino/webrtc', 'werift']) {
|
|
70
70
|
try {
|
|
71
|
-
const w = await import(
|
|
72
|
-
if (typeof w?.RTCPeerConnection === 'function') { _PC = w.RTCPeerConnection;
|
|
71
|
+
const w = await import(name)
|
|
72
|
+
if (typeof w?.RTCPeerConnection === 'function') { _PC = w.RTCPeerConnection; _PCLookedUp = true; return _PC }
|
|
73
73
|
} catch (_) { /* no está: se prueba el siguiente, y si no, el proxio (escalón 4) */ }
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
_PCLookedUp = true
|
|
76
76
|
return _PC
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/roadmap@0.4.
|
|
1
|
+
Copia vendorizada de @dotrino/roadmap@0.4.32 (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.
|
|
@@ -80,10 +80,10 @@ export function meets (m, { product, peer, version }) {
|
|
|
80
80
|
if (!productOf(m, product)) {
|
|
81
81
|
return { ok: false, reason: `the registry does not know "${product}"` }
|
|
82
82
|
}
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
if (satisfies(version,
|
|
86
|
-
return { ok: false, reason: `${product} needs ${peer} ${
|
|
83
|
+
const range = requirementFor(m, product, peer)
|
|
84
|
+
if (range == null) return { ok: true, reason: '' }
|
|
85
|
+
if (satisfies(version, range)) return { ok: true, reason: '' }
|
|
86
|
+
return { ok: false, reason: `${product} needs ${peer} ${range} and this one is ${version}` }
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/** La lista de rotas del registro, lista para pasarla a `check` de `@dotrino/compat`. */
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export const MANIFESTS = Object.freeze({
|
|
9
9
|
"dotrino": {
|
|
10
10
|
"manifest": "dotrino",
|
|
11
|
-
"seq":
|
|
11
|
+
"seq": 31,
|
|
12
12
|
"issued": "2026-10-16",
|
|
13
13
|
"note": "Registro comun de compatibilidad del ecosistema. Se edita a mano en este repo y se consulta con @dotrino/roadmap. Rangos: ver @dotrino/compat/ranges (x.y.z, >= > <= <, «a - b», «*», o una lista que es un O).",
|
|
14
14
|
"products": {
|
|
@@ -22,16 +22,16 @@ export const MANIFESTS = Object.freeze({
|
|
|
22
22
|
"env": {
|
|
23
23
|
"repo": "imdotrino/dotrino-vault",
|
|
24
24
|
"npm": "@dotrino/env",
|
|
25
|
-
"current": "0.
|
|
25
|
+
"current": "0.91.1",
|
|
26
26
|
"protocol": 1,
|
|
27
27
|
"requires": {
|
|
28
|
-
"vault": ">=0.
|
|
28
|
+
"vault": ">=0.91.1"
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"identity": {
|
|
32
32
|
"repo": "imdotrino/dotrino-identity",
|
|
33
33
|
"npm": "@dotrino/identity",
|
|
34
|
-
"current": "0.
|
|
34
|
+
"current": "0.120.0",
|
|
35
35
|
"protocol": 1,
|
|
36
36
|
"requires": {
|
|
37
37
|
"proxy-client": ">=0.13.1"
|
|
@@ -40,7 +40,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
40
40
|
"lobby": {
|
|
41
41
|
"repo": "imdotrino/dotrino-lobby",
|
|
42
42
|
"npm": "@dotrino/lobby",
|
|
43
|
-
"current": "0.10.
|
|
43
|
+
"current": "0.10.4",
|
|
44
44
|
"protocol": 2,
|
|
45
45
|
"requires": {
|
|
46
46
|
"proxy-client": ">=0.22.0",
|
|
@@ -85,7 +85,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
85
85
|
"notifications": {
|
|
86
86
|
"repo": "imdotrino/dotrino-notifications",
|
|
87
87
|
"npm": "@dotrino/notifications",
|
|
88
|
-
"current": "0.
|
|
88
|
+
"current": "0.7.1",
|
|
89
89
|
"protocol": 2,
|
|
90
90
|
"requires": {
|
|
91
91
|
"proxy-client": ">=0.20.0"
|
|
@@ -101,7 +101,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
101
101
|
"profile": {
|
|
102
102
|
"repo": "imdotrino/dotrino-profile",
|
|
103
103
|
"npm": "@dotrino/profile",
|
|
104
|
-
"current": "0.20.
|
|
104
|
+
"current": "0.20.2",
|
|
105
105
|
"protocol": 1,
|
|
106
106
|
"requires": {
|
|
107
107
|
"identity": ">=0.33.0"
|
|
@@ -109,7 +109,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
109
109
|
},
|
|
110
110
|
"proxy": {
|
|
111
111
|
"repo": "imdotrino/dotrino-proxy",
|
|
112
|
-
"current": "1.
|
|
112
|
+
"current": "1.7.0",
|
|
113
113
|
"protocol": 2,
|
|
114
114
|
"requires": {
|
|
115
115
|
"vault": ">=0.87.0",
|
|
@@ -121,7 +121,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
121
121
|
"proxy-client": {
|
|
122
122
|
"repo": "imdotrino/dotrino-proxy-client",
|
|
123
123
|
"npm": "@dotrino/proxy-client",
|
|
124
|
-
"current": "0.
|
|
124
|
+
"current": "0.38.2",
|
|
125
125
|
"protocol": 2,
|
|
126
126
|
"requires": {
|
|
127
127
|
"identity": ">=0.53.0",
|
|
@@ -131,10 +131,10 @@ export const MANIFESTS = Object.freeze({
|
|
|
131
131
|
"remote-agent": {
|
|
132
132
|
"repo": "imdotrino/dotrino-remote-agent",
|
|
133
133
|
"npm": "@dotrino/remote-agent",
|
|
134
|
-
"current": "0.
|
|
134
|
+
"current": "0.22.1",
|
|
135
135
|
"protocol": 1,
|
|
136
136
|
"requires": {
|
|
137
|
-
"identity": ">=0.
|
|
137
|
+
"identity": ">=0.120.0",
|
|
138
138
|
"vault": ">=0.85.0",
|
|
139
139
|
"proxy-client": ">=0.36.1",
|
|
140
140
|
"vaultd": ">=0.154.0"
|
|
@@ -163,7 +163,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
163
163
|
},
|
|
164
164
|
"terminal-android": {
|
|
165
165
|
"repo": "imdotrino/dotrino-terminal",
|
|
166
|
-
"current": "0.13.
|
|
166
|
+
"current": "0.13.4",
|
|
167
167
|
"protocol": 1,
|
|
168
168
|
"requires": {
|
|
169
169
|
"terminal-agent": ">=0.37.0"
|
|
@@ -193,11 +193,11 @@ export const MANIFESTS = Object.freeze({
|
|
|
193
193
|
"content": {
|
|
194
194
|
"repo": "imdotrino/dotrino-content",
|
|
195
195
|
"npm": "@dotrino/content",
|
|
196
|
-
"current": "0.6.
|
|
196
|
+
"current": "0.6.6",
|
|
197
197
|
"protocol": 1,
|
|
198
198
|
"requires": {
|
|
199
|
-
"remote-agent": ">=0.
|
|
200
|
-
"identity": ">=0.
|
|
199
|
+
"remote-agent": ">=0.22.0",
|
|
200
|
+
"identity": ">=0.120.0",
|
|
201
201
|
"vault": ">=0.87.0",
|
|
202
202
|
"proxy-client": ">=0.36.1"
|
|
203
203
|
}
|
|
@@ -205,7 +205,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
205
205
|
"sealers": {
|
|
206
206
|
"repo": "imdotrino/dotrino-sealers",
|
|
207
207
|
"npm": "@dotrino/sealers",
|
|
208
|
-
"current": "0.10.
|
|
208
|
+
"current": "0.10.1",
|
|
209
209
|
"protocol": 1,
|
|
210
210
|
"requires": {
|
|
211
211
|
"remote-agent": ">=0.20.1",
|
|
@@ -217,7 +217,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
217
217
|
"roadmap": {
|
|
218
218
|
"repo": "imdotrino/dotrino-roadmap",
|
|
219
219
|
"npm": "@dotrino/roadmap",
|
|
220
|
-
"current": "0.4.
|
|
220
|
+
"current": "0.4.32",
|
|
221
221
|
"protocol": 1,
|
|
222
222
|
"requires": {
|
|
223
223
|
"compat": "0.4.0+"
|
|
@@ -226,7 +226,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
226
226
|
"store": {
|
|
227
227
|
"repo": "imdotrino/dotrino-store",
|
|
228
228
|
"npm": "@dotrino/store",
|
|
229
|
-
"current": "0.12.
|
|
229
|
+
"current": "0.12.4",
|
|
230
230
|
"protocol": 1,
|
|
231
231
|
"requires": {
|
|
232
232
|
"identity": ">=0.91.0",
|
|
@@ -243,7 +243,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
243
243
|
"topbar": {
|
|
244
244
|
"repo": "imdotrino/dotrino-topbar",
|
|
245
245
|
"npm": "@dotrino/topbar",
|
|
246
|
-
"current": "0.16.
|
|
246
|
+
"current": "0.16.2",
|
|
247
247
|
"protocol": 1,
|
|
248
248
|
"requires": {
|
|
249
249
|
"identity": ">=0.96.1",
|
|
@@ -253,11 +253,11 @@ export const MANIFESTS = Object.freeze({
|
|
|
253
253
|
"vault": {
|
|
254
254
|
"repo": "imdotrino/dotrino-vault",
|
|
255
255
|
"npm": "@dotrino/vault",
|
|
256
|
-
"current": "0.
|
|
256
|
+
"current": "0.91.1",
|
|
257
257
|
"protocol": 1,
|
|
258
258
|
"requires": {
|
|
259
259
|
"vaultd": ">=0.105.0",
|
|
260
|
-
"proxy-client": ">=0.
|
|
260
|
+
"proxy-client": ">=0.38.0",
|
|
261
261
|
"identity": ">=0.118.0",
|
|
262
262
|
"opaque": ">=0.1.0",
|
|
263
263
|
"compat": ">=0.4.0",
|
|
@@ -269,11 +269,11 @@ export const MANIFESTS = Object.freeze({
|
|
|
269
269
|
"vaultd": {
|
|
270
270
|
"repo": "imdotrino/dotrino-vault",
|
|
271
271
|
"npm": "@dotrino/vaultd",
|
|
272
|
-
"current": "0.
|
|
272
|
+
"current": "0.163.4",
|
|
273
273
|
"protocol": 1,
|
|
274
274
|
"requires": {
|
|
275
275
|
"identity": ">=0.118.0",
|
|
276
|
-
"proxy-client": ">=0.
|
|
276
|
+
"proxy-client": ">=0.38.0",
|
|
277
277
|
"store": ">=0.11.0",
|
|
278
278
|
"passmanager": ">=0.18.4",
|
|
279
279
|
"compat": ">=0.4.0",
|
|
@@ -284,7 +284,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
284
284
|
"verifier": {
|
|
285
285
|
"repo": "imdotrino/dotrino-verifier",
|
|
286
286
|
"npm": "@dotrino/verifier",
|
|
287
|
-
"current": "0.3.
|
|
287
|
+
"current": "0.3.1",
|
|
288
288
|
"protocol": 1,
|
|
289
289
|
"requires": {
|
|
290
290
|
"identity": "0.87.0+"
|
|
@@ -293,7 +293,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
293
293
|
"passmanager": {
|
|
294
294
|
"repo": "imdotrino/dotrino-passmanager",
|
|
295
295
|
"npm": "@dotrino/passmanager",
|
|
296
|
-
"current": "0.18.
|
|
296
|
+
"current": "0.18.5",
|
|
297
297
|
"protocol": 1,
|
|
298
298
|
"requires": {
|
|
299
299
|
"identity": ">=0.117.2",
|
|
@@ -340,6 +340,16 @@ export const MANIFESTS = Object.freeze({
|
|
|
340
340
|
"identity": ">=0.110.1"
|
|
341
341
|
}
|
|
342
342
|
},
|
|
343
|
+
"app-ios": {
|
|
344
|
+
"repo": "imdotrino/dotrino-app",
|
|
345
|
+
"current": "0.4.13",
|
|
346
|
+
"protocol": 1,
|
|
347
|
+
"requires": {
|
|
348
|
+
"proxy": "1.4.7+",
|
|
349
|
+
"vaultd": ">=0.114.0",
|
|
350
|
+
"identity": ">=0.110.1"
|
|
351
|
+
}
|
|
352
|
+
},
|
|
343
353
|
"social-bot": {
|
|
344
354
|
"repo": "imdotrino/dotrino-social-bot",
|
|
345
355
|
"current": "0.3.0",
|
|
@@ -845,6 +855,17 @@ export const MANIFESTS = Object.freeze({
|
|
|
845
855
|
],
|
|
846
856
|
"why": "la bóveda recordaba el sí de aprobación por aparato: después de aprobar un borrado, o de leer una contraseña, ese aparato podía borrar o pisar una passkey sin que se pidiera nada ni le llegara aviso a nadie",
|
|
847
857
|
"fix": "sube a @dotrino/vaultd 0.160.2"
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
"product": "content",
|
|
861
|
+
"versions": [
|
|
862
|
+
"0.6.1",
|
|
863
|
+
"0.6.2",
|
|
864
|
+
"0.6.3",
|
|
865
|
+
"0.6.4"
|
|
866
|
+
],
|
|
867
|
+
"why": "el paquete no traía lib/cid.js, que lib/public.js importa: el node no arranca (ERR_MODULE_NOT_FOUND)",
|
|
868
|
+
"fix": "sube a @dotrino/content 0.6.6"
|
|
848
869
|
}
|
|
849
870
|
]
|
|
850
871
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/vault@0.
|
|
1
|
+
Copia vendorizada de @dotrino/vault@0.91.1 (dotrino-vault/lib/src/{admin,b64,browser,core/approvals,core/backup,core/blocked,core/browserDisk,core/commKey,core/disk,core/libVersion,core/machines,core/notices,core/notifiable,core/passwords,core/sealKey,core/sealer,core/sealers,core/secretsStore,core/sha256,core/store,core/subacta,core/threadStore,core/transport,core/vault,enroll,index,loginClient,passwordLogins,procContext,protocol,revocation,sealed}.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
|
browser.js es la bóveda de esta pestaña: el MISMO núcleo que el binario (core/), con
|
|
4
4
|
el disco en IndexedDB. Importa @dotrino/{compat,roadmap,passmanager,store/core} y
|
|
@@ -525,14 +525,14 @@ export function openSecretsStore (disk, { sealer = null, recipients = null, sign
|
|
|
525
525
|
const [kind, k] = splitOwner(owner)
|
|
526
526
|
const bag = kind === 'ns' ? data.ns : data.dev
|
|
527
527
|
const g = (bag[k]?.keyring || []).find((x) => x.gen === gen)
|
|
528
|
-
if (!g) throw new Error(`putWrap:
|
|
528
|
+
if (!g) throw new Error(`putWrap: generation ${gen} of ${owner} does not exist`)
|
|
529
529
|
if (!wrap?.epk || !wrap?.iv || !wrap?.ct) throw new Error('putWrap: envoltura mal formada')
|
|
530
530
|
// SOLO AÑADE, NUNCA PISA. Quien re-envuelve es un servicio, y un servicio no
|
|
531
531
|
// administra: si pudiera reemplazar una envoltura existente podría dejar sin leer
|
|
532
532
|
// a otro miembro —o a quien administra— con una envoltura basura, y eso es
|
|
533
533
|
// denegación de servicio disfrazada de reparto. Reemplazar es cosa de la bóveda,
|
|
534
534
|
// por el camino de escribir (que estrena generación entera).
|
|
535
|
-
if (g.wraps?.[memberPub]) throw new Error(`putWrap: ${memberPub.slice(0, 12)}…
|
|
535
|
+
if (g.wraps?.[memberPub]) throw new Error(`putWrap: ${memberPub.slice(0, 12)}… already has a wrap for generation ${gen}`)
|
|
536
536
|
g.wraps = { ...(g.wraps || {}), [memberPub]: wrap }
|
|
537
537
|
save()
|
|
538
538
|
return true
|
|
@@ -36,7 +36,7 @@ export async function masterPubkeyOf (identity) {
|
|
|
36
36
|
* @param {string} [opts.url] URL del proxy (default wss://proxy.dotrino.com).
|
|
37
37
|
* @returns {Promise<{ client, token:string, identify():Promise<void> }>}
|
|
38
38
|
*/
|
|
39
|
-
export async function createTransport ({ identity, url = DEFAULT_PROXY, commKey = null, log = () => {}, makeClient = null, announce = null, recheckMs = 60_000, patienceMs = 5 * 60_000, reannounceMs = 10 * 60_000 }) {
|
|
39
|
+
export async function createTransport ({ identity, url = DEFAULT_PROXY, commKey = null, log = () => {}, makeClient = null, announce = null, wakeUrl = null, recheckMs = 60_000, patienceMs = 5 * 60_000, reannounceMs = 10 * 60_000 }) {
|
|
40
40
|
// Import dinámico DESPUÉS de instalar los globals que el paquete usa.
|
|
41
41
|
// `WebSocketProxyClient` (la clase) y NO el helper `getWebSocketProxyClient`:
|
|
42
42
|
// ese es un SINGLETON de proceso, y con multi-perfil el vault necesita una
|
|
@@ -182,6 +182,25 @@ export async function createTransport ({ identity, url = DEFAULT_PROXY, commKey
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/**
|
|
186
|
+
* DESPIÉRTAME AQUÍ (una bóveda que no vive conectada, `lambda/`). Si la plataforma da una
|
|
187
|
+
* URL, se registra en el proxio con la llave de comunicación y el acta, para que llame a esa
|
|
188
|
+
* URL cuando llegue algo para la cuenta y no haya nadie suyo conectado. Va pegado a
|
|
189
|
+
* `identify` por lo mismo que el anuncio: el acta puede haber cambiado. Si falla, se dice:
|
|
190
|
+
* lo que se pierde es que la despierten, y eso no se puede callar.
|
|
191
|
+
*/
|
|
192
|
+
const registerWake = async () => {
|
|
193
|
+
if (!wakeUrl || !identificado) return
|
|
194
|
+
const comm = commKey?.pub?.() || null
|
|
195
|
+
if (!comm || !actaVista) { log('[vault] cannot register the wake URL: no communication key in the record yet'); return }
|
|
196
|
+
try {
|
|
197
|
+
await client.registerWake({ publicKey: comm, sign: (d) => commKey.sign(d), url: wakeUrl, acta: actaVista, channel: announce || undefined })
|
|
198
|
+
log('[vault] wake URL registered on the proxy')
|
|
199
|
+
} catch (e) {
|
|
200
|
+
log(`[vault] could not register the wake URL (${e.code || ''} ${e.message}) — the proxy will not wake this vault`)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
185
204
|
let reintento = null
|
|
186
205
|
let fallos = 0
|
|
187
206
|
const identifyWithRetry = async () => {
|
|
@@ -189,6 +208,7 @@ export async function createTransport ({ identity, url = DEFAULT_PROXY, commKey
|
|
|
189
208
|
try {
|
|
190
209
|
await identify()
|
|
191
210
|
await announceAccount()
|
|
211
|
+
await registerWake()
|
|
192
212
|
if (fallos) log(`[vault] identified on the proxy after ${fallos + 1} attempt(s)`)
|
|
193
213
|
fallos = 0
|
|
194
214
|
} catch (e) {
|
|
@@ -80,6 +80,8 @@ const MAX_REPLY_BYTES = 768 * 1024
|
|
|
80
80
|
* readProcContext(pid) lo que el sistema dice de un proceso; `null` si no se puede mirar
|
|
81
81
|
* openLocalDesk({ name, onMessage, channels, maxBytes, log }) el atajo en la misma máquina
|
|
82
82
|
* makeClient quién crea el cliente del proxio (opcional)
|
|
83
|
+
* wakeUrl la URL que despierta a esta bóveda si no vive conectada (opcional, `lambda/`)
|
|
84
|
+
* onActivity() llega un mensaje del proxio (opcional; la Lambda mide con esto su inactividad)
|
|
83
85
|
* noticeGroupMs cuánto se agrupan los avisos de cambio
|
|
84
86
|
* opaque el servidor de OPAQUE, si no vale el del paquete (opcional)
|
|
85
87
|
* servePasswords `false`: el almacén de contraseñas lo atiende quien monta la bóveda,
|
|
@@ -492,7 +494,7 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
|
|
|
492
494
|
// ninguna llave y solo sabe la dirección `nombre@AB12-CD34-EF56`— pueda encontrar a quien
|
|
493
495
|
// la atiende. Lo construye la pieza compartida, porque quien lo busca es la extensión.
|
|
494
496
|
const { client, identify: reidentificar, isIdentified } = await createTransport({
|
|
495
|
-
identity, url: proxyUrl, commKey, log, announce: vaultChannel(fp), makeClient: platform.makeClient || null
|
|
497
|
+
identity, url: proxyUrl, commKey, log, announce: vaultChannel(fp), makeClient: platform.makeClient || null, wakeUrl: platform.wakeUrl || null
|
|
496
498
|
})
|
|
497
499
|
|
|
498
500
|
// El registro público de cadenas de selladores: deposita, si hay a dónde, los eslabones
|
|
@@ -2189,7 +2191,12 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
|
|
|
2189
2191
|
reply(from, { type: MSG.INCIDENT_RESULT, id: pend.id, approvers, blocked: false })
|
|
2190
2192
|
}
|
|
2191
2193
|
|
|
2192
|
-
client.on('message', (from, payload) =>
|
|
2194
|
+
client.on('message', (from, payload) => {
|
|
2195
|
+
// Quien monta la bóveda puede querer saber que hay trabajo (la Lambda se apaga cuando
|
|
2196
|
+
// deja de haberlo). Es un aviso, no decide nada.
|
|
2197
|
+
try { platform.onActivity?.() } catch (_) {}
|
|
2198
|
+
onMessage(from, payload)
|
|
2199
|
+
})
|
|
2193
2200
|
|
|
2194
2201
|
// EL MOSTRADOR LOCAL, siempre que se pueda. Si no se puede abrir (un sistema sin sockets
|
|
2195
2202
|
// de archivo, un directorio de solo lectura) se dice y se sigue: los servicios usan el
|
|
@@ -44,6 +44,8 @@ const err = (code, message, extra = {}) => Object.assign(new Error(message), { c
|
|
|
44
44
|
|
|
45
45
|
/** Cuánto se espera a que una bóveda conteste antes de probar con la siguiente. */
|
|
46
46
|
export const REPLY_TIMEOUT_MS = 15_000
|
|
47
|
+
/** Lo que se espera a una bóveda que se está despertando: arrancar en frío y conectarse. */
|
|
48
|
+
export const WAKE_WAIT_MS = 30_000
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
51
|
* Escucha UNA respuesta de un token concreto. El `off` se suelta siempre —también al
|
|
@@ -98,7 +100,14 @@ export async function loginWithPassword ({ transport, address, password, label =
|
|
|
98
100
|
if (typeof password !== 'string' || !password) throw err('no-password', 'loginWithPassword: password required')
|
|
99
101
|
const { user, code, address: dir } = parseLoginAddress(address)
|
|
100
102
|
|
|
101
|
-
|
|
103
|
+
let tokens = await transport.list(vaultChannel(code))
|
|
104
|
+
// UNA BÓVEDA QUE DUERME (la de una Lambda): el proxio la está despertando y lo dice. Se
|
|
105
|
+
// espera a que se anuncie en vez de contestar que esa cuenta no existe.
|
|
106
|
+
const hasta = Date.now() + WAKE_WAIT_MS
|
|
107
|
+
while (!tokens.length && tokens.waking && Date.now() < hasta) {
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
109
|
+
tokens = await transport.list(vaultChannel(code))
|
|
110
|
+
}
|
|
102
111
|
if (!tokens.length) {
|
|
103
112
|
throw err('no-vault', 'no vault is answering for that address right now: turn yours on, or check the address')
|
|
104
113
|
}
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
* binario, `lib/src/index.js` en la pestaña—. Lo único que no es puro es `registerLogin`, al
|
|
25
25
|
* final, porque hace falta la identidad que firma.
|
|
26
26
|
*/
|
|
27
|
-
import { server as opaquePorDefecto, suiteId as suitePorDefecto } from '@dotrino/opaque'
|
|
28
|
-
import { pubkeyId } from '@dotrino/identity/capabilities'
|
|
27
|
+
import { server as opaquePorDefecto, client as opaqueClientePorDefecto, suiteId as suitePorDefecto } from '@dotrino/opaque'
|
|
28
|
+
import { pubkeyId, makeDeviceKey, makeDeviceEncKey } from '@dotrino/identity/capabilities'
|
|
29
29
|
import { DEVICE_CAPS, capScope } from '@dotrino/identity/acta'
|
|
30
30
|
import { deviceIdOf } from './enroll.js'
|
|
31
31
|
import { bytesToB64url, b64urlToBytes } from './b64.js'
|
|
@@ -448,6 +448,29 @@ export function createLoginDesk ({ load, save, now = () => Date.now(), opaque =
|
|
|
448
448
|
* `caps` los PERMISOS del acta que lleva el aparato, cualquiera de `DEVICE_CAPS`;
|
|
449
449
|
* por defecto firmar, leer y guardar
|
|
450
450
|
*/
|
|
451
|
+
/**
|
|
452
|
+
* DAR DE ALTA UN USUARIO, DEL LADO DE QUIEN TIENE LA CONTRASEÑA: el registro OPAQUE se
|
|
453
|
+
* termina aquí y las llaves del aparato nacen aquí y salen ya cerradas. La bóveda solo ve
|
|
454
|
+
* `begin`/`finish`, que es su mitad; quien llama decide por dónde llegan (el canal local de
|
|
455
|
+
* la CLI, o la propia bóveda en el arranque de una Lambda).
|
|
456
|
+
*
|
|
457
|
+
* @param {object} opts
|
|
458
|
+
* `begin(req)` → `{ response }` (`register-begin` de la bóveda)
|
|
459
|
+
* `finish(req)` → lo que devuelva (`register-finish` de la bóveda)
|
|
460
|
+
*/
|
|
461
|
+
export async function createPasswordLogin ({ user, password, label, caps, begin, finish, opaque = opaqueClientePorDefecto } = {}) {
|
|
462
|
+
if (!isValidUser(user)) throw Object.assign(new Error('invalid user name'), { code: 'bad-user' })
|
|
463
|
+
if (typeof password !== 'string' || !password) throw Object.assign(new Error('a password is required'), { code: 'no-password' })
|
|
464
|
+
const start = opaque.registrationStart({ password })
|
|
465
|
+
const begun = await begin({ user, request: start.request })
|
|
466
|
+
// La bóveda contesta con su mitad; el registro se termina AQUÍ, con la contraseña.
|
|
467
|
+
const fin = opaque.registrationFinish({ state: start.state, response: begun.response, password })
|
|
468
|
+
const device = await makeDeviceKey({ label })
|
|
469
|
+
const enc = await makeDeviceEncKey()
|
|
470
|
+
const blob = await sealDeviceKeys(fin.exportKey, { sign: device.privateJwk, enc: enc.privateJwk })
|
|
471
|
+
return finish({ user, upload: fin.upload, pub: device.publickey, encPub: enc.publickey, label, blob, caps })
|
|
472
|
+
}
|
|
473
|
+
|
|
451
474
|
export async function registerLogin ({
|
|
452
475
|
identity, logins, user, upload, pub, encPub = null, label = '', blob,
|
|
453
476
|
caps = ['sign', 'read', 'store'], scope, replace = false
|