@dotrino/vault 0.54.0 → 0.60.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/LICENSE CHANGED
File without changes
package/README.md CHANGED
File without changes
@@ -30,8 +30,9 @@ const flag = (name) => { const i = argv.indexOf('--' + name); return i >= 0 ? ar
30
30
  function help () {
31
31
  console.log(`dotrino-env — credenciales del vault en vez del .env
32
32
 
33
- enroll --vault <bóveda> --ns <ns> [--code <invitación>] [--dir <dir>]
34
- (--vault SOLO hace falta aquí, y para usarlo si el mismo <ns> está en dos bóvedas)
33
+ enroll --ns <ns> [--vault <bóveda>] [--code <invitación>] [--dir <dir>]
34
+ (sin --vault se usa el NOMBRE DE LA CUENTA, que la bóveda dice al enrolar; solo
35
+ hace falta darla si quieres otra etiqueta, o si el mismo <ns> está en dos bóvedas)
35
36
  Registra ESTE servicio contra el vault (una sola vez).
36
37
  Antes, en el vault: dotrino-vault pair --service <ns>
37
38
  Si no pasas --code (alias --qr), se pide por consola (también acepta stdin).
@@ -103,16 +104,45 @@ function etiquetaBoveda ({ exigida = false } = {}) {
103
104
  console.error(' Sirve para que dos bóvedas puedan tener un cajón llamado igual.')
104
105
  process.exit(2)
105
106
  }
106
- if (!isValidVaultLabel(v)) { console.error('etiqueta inválida: usa [a-z0-9-]{1,32}'); process.exit(2) }
107
- return v
107
+ // EN MINÚSCULAS, SIN PROTESTAR. La etiqueta es el nombre de una carpeta de esta máquina
108
+ // y nada más, así que las mayúsculas no significan nada aquí — pero el perfil sí se llama
109
+ // «Cepi», con mayúscula, y es lo que uno teclea. Rechazarlo era fricción sin nada a
110
+ // cambio, y el mensaje solo repetía la regla en vez de decir qué escribir.
111
+ const norm = v.toLowerCase()
112
+ if (!isValidVaultLabel(norm)) {
113
+ console.error('etiqueta inválida: «%s»', v)
114
+ console.error(' es el nombre de una carpeta de esta máquina: letras, números y guiones (máx. 32).')
115
+ console.error(' ej.: --vault cepi')
116
+ process.exit(2)
117
+ }
118
+ return norm
108
119
  }
109
120
 
110
121
  async function cmdEnroll () {
111
122
  const ns = flag('ns')
112
123
  if (!ns) { console.error('falta --ns <ns> (el mismo del `dotrino-vault pair --service <ns>`)'); process.exit(2) }
113
- const boveda = etiquetaBoveda({ exigida: true })
114
- const dir = flag('dir') || serviceDir(boveda, ns)
115
124
  const qr = parseInvite((flag('qr') || flag('code')) || await readInvite())
125
+ // SIN `--vault`, SE LO PREGUNTA A LA BÓVEDA (dueño, 2026-09-03: «en el enroll si no se
126
+ // pone vault debería usar el nombre del perfil»). Al enrolar ya sabes contra qué bóveda
127
+ // vas —la invitación lo dice, o ella lo contesta—, así que pedir la etiqueta era hacer
128
+ // teclear un dato que el programa tiene delante.
129
+ //
130
+ // La etiqueta es solo el nombre de una carpeta de esta máquina, así que el nombre de la
131
+ // cuenta se lleva a minúsculas y se le quitan los caracteres que no valen. Si de ahí no
132
+ // sale nada utilizable, se pide — no se inventa un nombre.
133
+ let boveda = etiquetaBoveda()
134
+ if (!boveda) {
135
+ const { vaultAccountName } = await import('../src/service.js')
136
+ const nombre = await vaultAccountName(qr).catch(() => '')
137
+ boveda = String(nombre || '').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32)
138
+ if (!boveda) {
139
+ console.error('esa bóveda no dice cómo se llama su cuenta: dime tú la etiqueta')
140
+ console.error(' ej.: dotrino-env enroll --ns %s --vault mi-boveda --code …', ns)
141
+ process.exit(2)
142
+ }
143
+ console.log(' bóveda: «%s» (el nombre de la cuenta; se guarda en ~/.dotrino/service/%s/%s)', boveda, boveda, ns)
144
+ }
145
+ const dir = flag('dir') || serviceDir(boveda, ns)
116
146
 
117
147
  console.log('\nEnrolando el servicio "%s" contra el vault…', ns)
118
148
  const { cert } = await enrollService({
@@ -150,7 +180,7 @@ async function cmdEnroll () {
150
180
  function cmdStatus () {
151
181
  const found = listEnrolled()
152
182
  if (!found.length) {
153
- console.log('Ningún servicio enrolado en %s\n Enrola uno: dotrino-env enroll --vault <bóveda> --ns <ns>', serviceRoot())
183
+ console.log('Ningún servicio enrolado en %s\n Enrola uno: dotrino-env enroll --ns <ns> --code "<invitación>"', serviceRoot())
154
184
  return
155
185
  }
156
186
  for (const e of found) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vault",
3
- "version": "0.54.0",
3
+ "version": "0.60.1",
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",
@@ -51,7 +51,7 @@
51
51
  ],
52
52
  "peerDependencies": {
53
53
  "@dotrino/identity": ">=0.73.1",
54
- "@dotrino/proxy-client": ">=0.13.0"
54
+ "@dotrino/proxy-client": ">=0.17.0"
55
55
  },
56
56
  "license": "MIT",
57
57
  "repository": {
package/src/admin.js CHANGED
@@ -56,7 +56,12 @@ export const ADMIN_OPS = Object.freeze([
56
56
  // aparato que administra no tiene sobres de lo privado (solo el servicio dueño y la
57
57
  // recuperación). Eso lo hace la bóveda en su máquina (`secret show/history/revert`).
58
58
  // No lo cablees.
59
- 'vars', 'var.set', 'var.setMany'
59
+ // `var.recipients` es solo LEER, y hace falta para poder escribir: la consola sella el
60
+ // valor ella misma (§8.1) y para eso necesita saber a quién envolverle la llave. Estaba
61
+ // documentada en `buildSealedVar` y la consola la llamaba, pero no existía — así que
62
+ // crear una variable desde la consola contestaba «admin: invalid operation». No expone
63
+ // nada nuevo: son llaves PÚBLICAS, las mismas que ya viajan en el acta.
64
+ 'vars', 'var.set', 'var.setMany', 'var.recipients'
60
65
  ])
61
66
 
62
67
  /**
@@ -85,7 +90,7 @@ export const ADMIN_OPS = Object.freeze([
85
90
  * Se dejan las de LEER a propósito: si se cortaran todas, la consola no podría ni
86
91
  * enterarse de por qué no funciona y parecería rota. Así puede decir «está cerrada».
87
92
  */
88
- export const ADMIN_OPS_WHILE_LOCKED = Object.freeze(['audit', 'vars', 'var.set', 'var.setMany'])
93
+ export const ADMIN_OPS_WHILE_LOCKED = Object.freeze(['audit', 'vars', 'var.set', 'var.setMany', 'var.recipients'])
89
94
 
90
95
  /** Cuánto se recuerda un nonce ya usado (el doble de la ventana de frescura). */
91
96
  export const ADMIN_NONCE_TTL_MS = 10 * 60 * 1000
@@ -195,7 +200,7 @@ export function createAdminDesk ({
195
200
 
196
201
  // VARIABLES DE ENTORNO. El módulo solo enruta y audita: qué valor puede salir y con
197
202
  // qué se cifra lo decide la bóveda (`vars`), que es la que tiene la clave y el disco.
198
- if (data.op === 'vars' || data.op === 'var.set' || data.op === 'var.setMany') {
203
+ if (data.op === 'vars' || data.op === 'var.set' || data.op === 'var.setMany' || data.op === 'var.recipients') {
199
204
  if (!vars) return { ok: false, error: 'admin: this vault does not serve environment variables' }
200
205
  // Un destino y solo uno: o un scope, o un aparato. Sin esto, mandar los dos dejaría
201
206
  // que quien llama adivine dónde acabó su variable.
@@ -212,6 +217,27 @@ export function createAdminDesk ({
212
217
  }
213
218
  if (toScope === toDevice) return { ok: false, error: 'admin: var.set needs exactly one target (ns or pub)' }
214
219
 
220
+ // A QUIÉN HAY QUE ENVOLVERLE. Se pide ANTES de sellar, porque sellar lo hace la
221
+ // consola: envolver solo necesita públicas, así que puede hacerlo un navegador sin
222
+ // que ninguna privada ande suelta (§8.1). La lista sale del ACTA y la sabe la
223
+ // bóveda, no la consola — así conceder un permiso a un aparato nuevo basta, sin que
224
+ // nadie tenga que acordarse de actualizar una lista en otro sitio.
225
+ //
226
+ // Depende de la VISIBILIDAD: una pública se envuelve además para quien administra,
227
+ // así que quien pregunta dice cuál va a escribir.
228
+ if (data.op === 'var.recipients') {
229
+ if (typeof vars.recipients !== 'function') {
230
+ return { ok: false, error: 'admin: this vault cannot say who to seal for (update it)' }
231
+ }
232
+ const result = await vars.recipients({
233
+ ns: toScope ? data.ns : null,
234
+ pub: toDevice ? data.pub : null,
235
+ public: data.public === true
236
+ })
237
+ audit('admin.var.recipients', { by, ns: toScope ? data.ns : null })
238
+ return { ok: true, result }
239
+ }
240
+
215
241
  // VARIAS DE UNA VEZ. Mismo permiso y misma frontera que una sola: lo que cambia es
216
242
  // que la bóveda las guarda juntas y manda UN aviso de cambio en vez de uno por
217
243
  // variable — o sea, el servicio se reinicia una vez, con la configuración entera,
package/src/atrest.js CHANGED
File without changes
package/src/config.js CHANGED
File without changes
package/src/enroll.js CHANGED
File without changes
package/src/env.js CHANGED
File without changes
package/src/envtext.js CHANGED
File without changes
package/src/index.js CHANGED
@@ -84,7 +84,10 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
84
84
  const client = injectedClient || await (async () => {
85
85
  const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
86
86
  const c = new WebSocketProxyClient({
87
- url: proxy, enableWebRTC: false, autoReconnect: true,
87
+ // WEBRTC SOLO DONDE EXISTE (ver `lib/src/service.js`): en Node no hay
88
+ // `RTCPeerConnection` y encenderlo reventaría al negociar; en un navegador es nativo y es
89
+ // el camino directo que hay que preferir. Se mira, en vez de apagarlo para siempre.
90
+ url: proxy, enableWebRTC: typeof globalThis.RTCPeerConnection === 'function', autoReconnect: true,
88
91
  maxReconnectAttempts: 100000, reconnectDelay: 4000
89
92
  })
90
93
  await c.connect()
package/src/invite.js CHANGED
File without changes
package/src/kek.js CHANGED
File without changes
@@ -0,0 +1,96 @@
1
+ /**
2
+ * localdesk.js — HABLARLE A LA BÓVEDA SIN SALIR DE LA MÁQUINA.
3
+ *
4
+ * Un servicio en el mismo equipo que la bóveda daba la vuelta por `proxy.dotrino.com`:
5
+ * salía a internet, volvía, y si el proxio tenía un mal momento el arranque se retrasaba
6
+ * cinco segundos por nada. Absurdo cuando los dos procesos comparten disco (dueño,
7
+ * 2026-09-03: «estando en la misma máquina debería ser inmediato»).
8
+ *
9
+ * Es EL MISMO PROTOCOLO. Lo que viaja es idéntico —`{type, data, signature, cert}`— y la
10
+ * bóveda lo comprueba igual, contra el certificado y contra el acta. **Alcanzar el socket
11
+ * no autoriza nada**: es la misma puerta por un camino más corto, no una puerta de atrás.
12
+ *
13
+ * Se hace pasar por un cliente del proxio (`send`, `sendByPubkey`, `on('message')`,
14
+ * `close`) para que quien lo use no tenga que saber por dónde va. `sendByPubkey` ignora a
15
+ * quién se dirige, y es correcto: al otro lado del socket solo hay una bóveda, la de esta
16
+ * máquina — si hubiera que elegir destinatario, este no sería el camino.
17
+ */
18
+ import fs from 'node:fs'
19
+ import net from 'node:net'
20
+ import path from 'node:path'
21
+ import os from 'node:os'
22
+
23
+ /**
24
+ * DÓNDE ESTÁ EL SOCKET, y por qué se nombra por la LLAVE de la bóveda.
25
+ *
26
+ * Una máquina puede tener varias bóvedas (varios perfiles), y un servicio no sabe nada de
27
+ * perfiles: lo único que conoce de la suya es su llave maestra, que lleva pineada desde
28
+ * que se enroló. Así que el nombre sale de ahí y los dos lados lo calculan igual, sin
29
+ * mirar ningún índice ni depender de cómo estén repartidos los datos en el disco.
30
+ *
31
+ * Y va en el directorio de EJECUCIÓN del usuario, no entre los datos: un socket es algo
32
+ * vivo, no algo guardado. Se lo lleva el reinicio, que es lo correcto.
33
+ */
34
+ export function socketDir () {
35
+ const base = process.env.XDG_RUNTIME_DIR || os.tmpdir()
36
+ return path.join(base, 'dotrino-vault')
37
+ }
38
+
39
+ export function localSocketPath (masterId) {
40
+ if (process.env.DOTRINO_VAULT_SOCKET) return process.env.DOTRINO_VAULT_SOCKET
41
+ return path.join(socketDir(), String(masterId) + '.sock')
42
+ }
43
+
44
+ /** ¿Hay una bóveda escuchando aquí mismo? Mirar el archivo es barato y no conecta nada. */
45
+ export const hasLocalVault = (masterId) => {
46
+ try { return fs.statSync(localSocketPath(masterId)).isSocket() } catch (_) { return false }
47
+ }
48
+
49
+ /**
50
+ * Se conecta al mostrador local y devuelve algo con la forma de un cliente del proxio.
51
+ * Si no hay socket, o no acepta, LANZA — no se queda callado: quien llama decide si sale
52
+ * por el proxio, y esa decisión tiene que ser explícita y no un silencio.
53
+ */
54
+ export async function connectLocal ({ masterId, timeoutMs = 3000 } = {}) {
55
+ const socketPath = localSocketPath(masterId)
56
+ const sock = await new Promise((resolve, reject) => {
57
+ const s = net.createConnection(socketPath)
58
+ const t = setTimeout(() => { s.destroy(); reject(new Error('the local vault desk did not answer')) }, timeoutMs)
59
+ s.once('connect', () => { clearTimeout(t); resolve(s) })
60
+ s.once('error', (e) => { clearTimeout(t); reject(new Error('no local vault desk at ' + socketPath + ': ' + e.message)) })
61
+ })
62
+
63
+ const oyentes = new Set()
64
+ let buf = ''
65
+ sock.setEncoding('utf8')
66
+ sock.on('data', (chunk) => {
67
+ buf += chunk
68
+ let i
69
+ while ((i = buf.indexOf('\n')) >= 0) {
70
+ const linea = buf.slice(0, i); buf = buf.slice(i + 1)
71
+ if (!linea.trim()) continue
72
+ let p = null
73
+ try { p = JSON.parse(linea) } catch (_) { continue }
74
+ // `from` es el mismo para todo: al otro lado solo hay una bóveda.
75
+ for (const fn of oyentes) { try { fn('local', p) } catch (_) {} }
76
+ }
77
+ })
78
+
79
+ const enviar = (obj) => { sock.write(JSON.stringify(obj) + '\n') }
80
+ return {
81
+ local: true,
82
+ token: 'local',
83
+ send: (_to, obj) => enviar(obj),
84
+ sendByPubkey: (_pub, obj) => enviar(obj),
85
+ // El mostrador local no pide identificarse: la puerta la abre el certificado que va
86
+ // dentro de cada mensaje, igual que por el proxio. Identificarse ahí sirve para ser
87
+ // DIRECCIONABLE, y aquí no hay a quién direccionar.
88
+ identify: async () => ({ ok: true }),
89
+ on: (ev, fn) => {
90
+ if (ev !== 'message') return () => {}
91
+ oyentes.add(fn)
92
+ return () => oyentes.delete(fn)
93
+ },
94
+ close: () => { try { sock.destroy() } catch (_) {} }
95
+ }
96
+ }
package/src/lock.js CHANGED
File without changes
package/src/protocol.js CHANGED
File without changes
package/src/revocation.js CHANGED
File without changes
package/src/sealed.js CHANGED
File without changes
package/src/service.js CHANGED
@@ -30,6 +30,7 @@ import { MSG, secretsScope, isValidSecretsNs } from './protocol.js'
30
30
  import { makeEphemeralKey, openSealed } from './sealed.js'
31
31
  import { parseInvite } from './invite.js'
32
32
  import { atRestFor } from './atrest.js'
33
+ import { connectLocal, hasLocalVault } from './localdesk.js'
33
34
 
34
35
  const IDENTITY_FILE = 'service-identity.json'
35
36
  const FRESH_WINDOW_MS = 5 * 60 * 1000
@@ -126,10 +127,33 @@ async function resolveAppointment (client, code) {
126
127
  return r.instance
127
128
  }
128
129
 
129
- async function freshClient (proxyUrl, connectTimeoutMs = 20000) {
130
+ /**
131
+ * EL MÁS CORTO PRIMERO: si la bóveda está en ESTA máquina, se le habla por su socket y no
132
+ * se sale a internet (dueño, 2026-09-03: «estando en la misma máquina debería ser
133
+ * inmediato»). Es el mismo protocolo y las mismas comprobaciones; lo único que cambia es
134
+ * por dónde van los bytes.
135
+ *
136
+ * No es un repliegue que tape nada: el socket o está o no está, y no estar es el caso
137
+ * normal —la bóveda suele vivir en otra máquina—. Lo que sí sería un repliegue es callar
138
+ * un fallo del socket y salir por el proxio como si nada, así que se dice.
139
+ */
140
+ async function freshClient (proxyUrl, connectTimeoutMs = 20000, master = null) {
130
141
  installNodeGlobals()
142
+ // El id sale de la LLAVE de la bóveda, que es lo único que quien llama sabe seguro de
143
+ // ella. Sin llave no se busca socket: no habría forma de saber cuál es el suyo.
144
+ const masterId = master ? (await pubkeyId(master)).slice(0, 16) : null
145
+ if (masterId && hasLocalVault(masterId)) {
146
+ try { return await connectLocal({ masterId }) } catch (e) {
147
+ console.error('[vault] the local desk is there but did not answer (%s): going out through the proxy', e.message)
148
+ }
149
+ }
131
150
  const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
132
- const client = new WebSocketProxyClient({ url: proxyUrl, enableWebRTC: false, autoReconnect: false })
151
+ // WEBRTC SOLO DONDE EXISTE. En Node no hay `RTCPeerConnection`, así que encenderlo
152
+ // reventaría al negociar; en un navegador es nativo y es el camino directo que hay que
153
+ // preferir. Se mira si está, en vez de apagarlo a mano para siempre: el día que este
154
+ // código corra en un navegador —o que Node lo traiga— se enciende solo.
155
+ const hayWebRTC = typeof globalThis.RTCPeerConnection === 'function'
156
+ const client = new WebSocketProxyClient({ url: proxyUrl, enableWebRTC: hayWebRTC, autoReconnect: false })
133
157
  // connect() del cliente solo se resuelve con 'connected' y solo rechaza en el
134
158
  // evento 'error' de transporte: si el socket cierra LIMPIO antes de 'connected'
135
159
  // (p.ej. el proxy banea la IP y envía close 1008), la promesa quedaría colgada
@@ -251,6 +275,43 @@ function writeServiceIdentity (dir, obj) {
251
275
  * puede convertirse en leer ni por descuido ni por un cambio futuro. Es el mismo cerrojo
252
276
  * que tiene la llave de comunicación.
253
277
  */
278
+ /**
279
+ * QR CORTO: se le pregunta a la bóveda QUIÉN ES, punto a punto, presentando el `sn`.
280
+ *
281
+ * Sale de dentro de `enrollWithVault` para poder preguntarlo ANTES de enrolar: quien
282
+ * enrola necesita el nombre de la cuenta para saber dónde guardar (la etiqueta de bóveda
283
+ * es una carpeta con ese nombre), y pedírselo al usuario cuando la bóveda ya lo sabe es
284
+ * fricción sin nada a cambio.
285
+ *
286
+ * Devuelve el mismo `qr` con `iss`, `proxy` y `acct` puestos. Con un QR largo no pregunta
287
+ * nada: ya los trae.
288
+ */
289
+ export async function sayHello (client, qr) {
290
+ if (qr.iss) return qr
291
+ const target = await resolveAppointment(client, qr.conn)
292
+ const hello = await new Promise((resolve, reject) => {
293
+ const off = client.on('message', (_f, p) => {
294
+ if (p?.type === MSG.HELLO_OK) { finish(); verifyHello(p, qr.sn).then(resolve, reject) }
295
+ else if (p?.type === MSG.ERROR) { finish(); reject(new Error(p.error)) }
296
+ })
297
+ const t = setTimeout(() => { finish(); reject(new Error('the vault did not answer: that code may have expired')) }, 15000)
298
+ const finish = () => { off(); clearTimeout(t) }
299
+ try { client.send(target, { type: MSG.HELLO, sn: qr.sn }) } catch (e) { finish(); reject(e) }
300
+ })
301
+ return { ...qr, iss: hello.iss, proxy: hello.proxy || qr.proxy, ...(hello.acct ? { acct: hello.acct } : {}) }
302
+ }
303
+
304
+ /**
305
+ * Cómo se llama la cuenta de esa invitación, sin enrolar nada. Una conexión y una pregunta.
306
+ */
307
+ export async function vaultAccountName (qr) {
308
+ if (typeof qr === 'string') qr = parseInvite(qr)
309
+ if (!qr?.sn) throw new Error('that does not look like a vault invitation')
310
+ if (qr.acct) return qr.acct
311
+ const client = await freshClient(qr.proxy || 'wss://proxy.dotrino.com')
312
+ try { return (await sayHello(client, qr)).acct || '' } finally { try { client.close() } catch (_) {} }
313
+ }
314
+
254
315
  export async function enrollWithVault ({ qr, label = 'agent', expectedScope = null, onCode, approveTimeoutMs = 180000, withEncKey = true } = {}) {
255
316
  // `parseInvite` y NO `JSON.parse`: el vault no imprime JSON desde hace rato.
256
317
  // `dotrino-vault pair` emite la URL del QR y el código compacto (`c…`/`t…`, ver
@@ -264,20 +325,7 @@ export async function enrollWithVault ({ qr, label = 'agent', expectedScope = nu
264
325
  rejectAdoption(qr.m)
265
326
 
266
327
  const client = await freshClient(qr.proxy || 'wss://proxy.dotrino.com')
267
- // QR CORTO: se le pregunta a la bóveda quién es, punto a punto, presentando el `sn`.
268
- if (!qr.iss) {
269
- const target = await resolveAppointment(client, qr.conn)
270
- const hello = await new Promise((resolve, reject) => {
271
- const off = client.on('message', (_f, p) => {
272
- if (p?.type === MSG.HELLO_OK) { finish(); verifyHello(p, qr.sn).then(resolve, reject) }
273
- else if (p?.type === MSG.ERROR) { finish(); reject(new Error(p.error)) }
274
- })
275
- const t = setTimeout(() => { finish(); reject(new Error('the vault did not answer: that code may have expired')) }, 15000)
276
- const finish = () => { off(); clearTimeout(t) }
277
- try { client.send(target, { type: MSG.HELLO, sn: qr.sn }) } catch (e) { finish(); reject(e) }
278
- })
279
- qr = { ...qr, iss: hello.iss, proxy: hello.proxy || qr.proxy }
280
- }
328
+ qr = await sayHello(client, qr)
281
329
  try {
282
330
  const device = await makeDeviceKey({ label })
283
331
  // La llave de CIFRADO: es a la que la bóveda sella cada variable. Sin ella el
@@ -413,7 +461,7 @@ export async function registerEncKey ({ dir, ns, proxyUrl, masterPubkey, device,
413
461
  cert = cert || saved?.cert
414
462
  if (!proxyUrl || !masterPubkey || !device || !cert) throw new Error('service not enrolled')
415
463
 
416
- const client = await freshClient(proxyUrl)
464
+ const client = await freshClient(proxyUrl, 20000, masterPubkey)
417
465
  try {
418
466
  await identifyAsService(client, device)
419
467
  const data = { op: 'enckey', ns, encPub, publickey: device.publickey, ts: Date.now() }
@@ -449,7 +497,7 @@ export async function fetchSecrets ({ dir, ns, proxyUrl, masterPubkey, device, c
449
497
  throw new Error('service not enrolled: run enrollService() first (service-identity.json missing)')
450
498
  }
451
499
 
452
- const client = await freshClient(proxyUrl)
500
+ const client = await freshClient(proxyUrl, 20000, masterPubkey)
453
501
  try {
454
502
  await identifyAsService(client, device)
455
503
 
@@ -1062,6 +1110,15 @@ export async function watchSecretsChanges ({
1062
1110
  const connect = async () => {
1063
1111
  if (stopped) return
1064
1112
  try {
1113
+ // POR EL PROXIO, A PROPÓSITO, aunque la bóveda esté en esta misma máquina.
1114
+ //
1115
+ // El mostrador local es pregunta y respuesta. Los avisos que la bóveda manda POR SU
1116
+ // CUENTA —«tu configuración cambió», una revocación— salen por el proxio, así que un
1117
+ // oyente conectado solo en local no se enteraría de nada y se quedaría corriendo con
1118
+ // la configuración vieja para siempre. Lo cazaron los E2E de avisos.
1119
+ //
1120
+ // El atajo local es para el ARRANQUE, que es donde se nota la latencia: pedir las
1121
+ // claves una vez. Escuchar es otra cosa y necesita que la bóveda pueda alcanzarte.
1065
1122
  client = await freshClient(saved.proxy)
1066
1123
  await identifyAsService(client, saved.device)
1067
1124
  client.on('message', (_from, p) => { handleMessage(p).catch(() => {}) })
package/src/sshAgent.js CHANGED
File without changes
package/src/sshKeys.js CHANGED
File without changes