@dotrino/vault 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vault",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Usa ESTE dispositivo (navegador) como bóveda/CA del ecosistema Dotrino: atiende enrolamientos por el proxy y firma certificados de delegación a tus máquinas. Incluye el cliente de SERVICIO (Node): un proyecto se enrola una vez y jala sus credenciales del vault en vez del .env (`import '@dotrino/vault/config'`).",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -43,7 +43,7 @@
43
43
  ],
44
44
  "peerDependencies": {
45
45
  "@dotrino/identity": ">=0.30.0",
46
- "@dotrino/proxy-client": ">=0.6.0"
46
+ "@dotrino/proxy-client": ">=0.9.0"
47
47
  },
48
48
  "license": "MIT",
49
49
  "repository": {
package/src/enroll.js CHANGED
@@ -146,14 +146,20 @@ export function createEnrollDesk ({
146
146
  * aviso. Es ORIENTATIVO (un nombre que puso su dueño); la
147
147
  * identidad de verdad de la cuenta es `iss`.
148
148
  */
149
- function startPairing ({ scope = defaultScope, ttlMs = defaultTtlMs, label = '', mode = 'join', account = '' } = {}) {
149
+ async function startPairing ({ scope = defaultScope, ttlMs = defaultTtlMs, label = '', mode = 'join', account = '' } = {}) {
150
150
  pending.clear() // uno a la vez: una sesión nueva supersede a la anterior
151
151
  const acct = String(account || '').slice(0, 40)
152
- // INVITACIÓN CORTA: si sabemos nuestra dirección en el proxy, el QR lleva solo
153
- // eso y el nonce de la sesión (13 bytes). La llave, el proxy y el nombre de la
154
- // cuenta los pide el aparato por la red presentando el `sn`. El nonce hace de
155
- // identificador de sesión: no hace falta un token de emparejamiento aparte.
156
- const conn = typeof connToken === 'function' ? connToken() : connToken
152
+ // INVITACIÓN CORTA: si sabemos cómo alcanzarnos, el QR lleva solo eso y el
153
+ // nonce de la sesión. La llave, el proxy y el nombre de la cuenta los pide el
154
+ // aparato por la red presentando el `sn`. El nonce hace de identificador de
155
+ // sesión: no hace falta un token de emparejamiento aparte.
156
+ //
157
+ // `conn` es una CITA del proxio (6 caracteres, un solo uso, caduca en
158
+ // minutos), no la dirección de la conexión: esa pasó a ser una instancia de
159
+ // 24 caracteres, que ni entra cómoda en un QR ni tiene por qué quedar impresa
160
+ // en algo que circula. Por eso se pide una nueva por emparejamiento, y por
161
+ // eso esto es asíncrono.
162
+ const conn = typeof connToken === 'function' ? await connToken() : connToken
157
163
  if (conn) {
158
164
  const sn = randToken(8)
159
165
  pending.set(sn, { token: sn, exp: Date.now() + PAIRING_TTL_MS, scope, ttlMs, label, sn, mode, account: acct, state: 'AWAITING_ENROLL' })
package/src/invite.js CHANGED
@@ -56,9 +56,12 @@ export const FMT_COMPACT = 'c'
56
56
  * que es lo único que de verdad decide. Esconderla no aportaba nada —una pública es
57
57
  * pública— y ocupaba 44 de los ~100 caracteres.
58
58
  *
59
- * `token` aquí NO es el de la sesión de emparejamiento: es el **token de conexión**
60
- * que el proxy le da a la bóveda (4 caracteres), o sea su dirección. Con eso el
61
- * aparato le habla directo, punto a punto, sin resolver nada.
59
+ * Lo que lleva NO es la dirección de la bóveda: es una **cita del proxy** —un código
60
+ * de 6 caracteres, de un solo uso y con minutos de vida, cuyos 2 primeros dicen qué
61
+ * proxio la emitió—. El aparato la CANJEA (`redeemPairingCode`) y obtiene la dirección
62
+ * real de la conexión, que hoy son 24 caracteres para poder rutearse entre proxios y no
63
+ * cabría cómoda en un QR. Además, una cita caduca y se quema: no deja una dirección
64
+ * permanente impresa en algo que circula.
62
65
  */
63
66
  export const FMT_SHORT = 't'
64
67
 
@@ -293,7 +296,10 @@ function compactDecode (text) {
293
296
  * Le pasa a cualquiera con proxy propio (y lo cazó el E2E de secretos, que levanta
294
297
  * uno local).
295
298
  */
296
- const CONN_TOKEN_LEN = 4
299
+ // La CITA del proxio: 2 caracteres de prefijo de nodo + 4 = 6. Antes eran 4 (la
300
+ // dirección de la conexión). Cambió porque la dirección pasó a ser una instancia
301
+ // de 24 caracteres, y lo que se imprime en un QR ahora es una cita de un solo uso.
302
+ const CONN_TOKEN_LEN = 6
297
303
 
298
304
  function shortEncode (qr) {
299
305
  if (!qr || qr.v !== 2) return null
package/src/service.js CHANGED
@@ -77,6 +77,25 @@ async function verificarHola (p, sn) {
77
77
  return b
78
78
  }
79
79
 
80
+ /**
81
+ * Canjea la cita del QR y devuelve la instancia a la que apunta.
82
+ *
83
+ * Una cita se quema al usarse y caduca en minutos, así que un error acá casi
84
+ * siempre significa lo mismo para quien lo lee: el código ya se usó o venció, y
85
+ * hay que pedir otro en la bóveda. Se dice así, no con el error crudo.
86
+ */
87
+ async function resolverCita (client, code) {
88
+ if (!code) throw new Error('la invitación no trae código de emparejamiento')
89
+ if (typeof client.redeemPairingCode !== 'function') {
90
+ throw new Error('el proxio no soporta códigos de emparejamiento (actualizá @dotrino/proxy-client)')
91
+ }
92
+ const r = await client.redeemPairingCode(code)
93
+ if (!r?.ok || !r.instance) {
94
+ throw new Error(`ese código no sirve: ${r?.error || 'no válido'}. Pedí uno nuevo en la bóveda.`)
95
+ }
96
+ return r.instance
97
+ }
98
+
80
99
  async function freshClient (proxyUrl, connectTimeoutMs = 20000) {
81
100
  installNodeGlobals()
82
101
  const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
@@ -157,6 +176,11 @@ export async function enrollService ({ qr, ns, dir, label, onCode, approveTimeou
157
176
  const client = await freshClient(qr.proxy || 'wss://proxy.dotrino.com')
158
177
  // QR CORTO: se le pregunta a la bóveda quién es, punto a punto, presentando el `sn`.
159
178
  if (!qr.iss) {
179
+ // `qr.conn` es una CITA (código de 6 caracteres, un solo uso): hay que
180
+ // canjearla para saber a qué conexión apunta. El canje lo resuelve el proxio
181
+ // que la emitió —lo dice el prefijo del propio código—, así que funciona
182
+ // aunque la bóveda esté en otro proxio de la malla.
183
+ const destino = await resolverCita(client, qr.conn)
160
184
  const hola = await new Promise((resolve, reject) => {
161
185
  const off = client.on('message', (_f, p) => {
162
186
  if (p?.type === MSG.HELLO_OK) { fin(); verificarHola(p, qr.sn).then(resolve, reject) }
@@ -164,7 +188,7 @@ export async function enrollService ({ qr, ns, dir, label, onCode, approveTimeou
164
188
  })
165
189
  const t = setTimeout(() => { fin(); reject(new Error('la bóveda no contestó: ese código pudo caducar')) }, 15000)
166
190
  const fin = () => { off(); clearTimeout(t) }
167
- try { client.send(qr.conn, { type: MSG.HELLO, sn: qr.sn }) } catch (e) { fin(); reject(e) }
191
+ try { client.send(destino, { type: MSG.HELLO, sn: qr.sn }) } catch (e) { fin(); reject(e) }
168
192
  })
169
193
  qr = { ...qr, iss: hola.iss, proxy: hola.proxy || qr.proxy }
170
194
  }