@dotrino/vaultd 0.162.3 → 0.163.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.
@@ -5,4 +5,4 @@
5
5
  * si no coincide con la del paquete: una versión anunciada que no es la que corre es peor
6
6
  * que no anunciar ninguna.
7
7
  */
8
- export const LIB_VERSION = '0.90.3'
8
+ export const LIB_VERSION = '0.91.0'
@@ -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) => onMessage(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
package/lib/src/kek.js CHANGED
@@ -85,7 +85,7 @@ export function writeConfig (dir, cfg) {
85
85
  * es a propósito el más tonto posible — cada CLI de KMS tiene sus manías con los binarios
86
86
  * y sus flags, y se resuelven en un envoltorio de tres líneas en vez de aquí dentro.
87
87
  */
88
- function runStep (step, input, what) {
88
+ export function runStep (step, input, what) {
89
89
  if (!step || typeof step.cmd !== 'string' || !step.cmd) {
90
90
  throw new KekError('kek-config', `kek provider "command": missing ${what}.cmd`)
91
91
  }
@@ -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
- const tokens = await transport.list(vaultChannel(code))
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vaultd",
3
- "version": "0.162.3",
3
+ "version": "0.163.1",
4
4
  "type": "module",
5
5
  "description": "Certificador personal de Dotrino: daemon headless que custodia la clave maestra y delega capacidades a tus dispositivos por el proxy. Tu CA propia.",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "@dotrino/identity": "0.120.0",
26
26
  "@dotrino/opaque": "0.1.0",
27
27
  "@dotrino/passmanager": "0.18.4",
28
- "@dotrino/proxy-client": "0.36.2",
28
+ "@dotrino/proxy-client": "0.38.0",
29
29
  "@dotrino/roadmap": "0.4.30",
30
30
  "@dotrino/store": "0.12.3",
31
31
  "@dotrino/update": "0.9.0",
package/src/ctl.js CHANGED
@@ -1706,21 +1706,12 @@ async function cmdLogins (rest) {
1706
1706
  if (a[0] === '+') caps.add(cap); else caps.delete(cap)
1707
1707
  }
1708
1708
  const label = nombre.join(' ').trim() || 'equipo prestado'
1709
- const { client: opaqueClient } = await import('@dotrino/opaque')
1710
- const { makeDeviceKey, makeDeviceEncKey } = await import('@dotrino/identity/capabilities')
1711
- const { sealDeviceKeys } = await import('../lib/src/passwordLogins.js')
1709
+ const { createPasswordLogin } = await import('../lib/src/passwordLogins.js')
1712
1710
  const password = await askNewPassword(user)
1713
-
1714
- const start = opaqueClient.registrationStart({ password })
1715
- const begun = await loginsRequest('register-begin', { user, request: start.request })
1716
- // La bóveda contesta con su mitad; el registro se termina AQUÍ, con la contraseña.
1717
- const fin = opaqueClient.registrationFinish({ state: start.state, response: begun.response, password })
1718
- // Las llaves del aparato NACEN en esta terminal y salen de aquí ya cerradas.
1719
- const device = await makeDeviceKey({ label })
1720
- const enc = await makeDeviceEncKey()
1721
- const blob = await sealDeviceKeys(fin.exportKey, { sign: device.privateJwk, enc: enc.privateJwk })
1722
- const r = await loginsRequest('register-finish', {
1723
- user, upload: fin.upload, pub: device.publickey, encPub: enc.publickey, label, blob, caps: [...caps]
1711
+ const r = await createPasswordLogin({
1712
+ user, password, label, caps: [...caps],
1713
+ begin: (req) => loginsRequest('register-begin', req),
1714
+ finish: (req) => loginsRequest('register-finish', req)
1724
1715
  })
1725
1716
  const { loginAddress } = await import('../lib/src/passwordLogins.js')
1726
1717
  console.log('\nListo. Se entra con esta dirección y esa contraseña:')
package/src/vault.js CHANGED
@@ -7,8 +7,12 @@ import { startVault as startVaultCore } from '../lib/src/core/vault.js'
7
7
  import { nodePlatform } from './platform.js'
8
8
  import { dataDir } from './paths.js'
9
9
 
10
- export async function startVault ({ dir = dataDir(), ...opts } = {}) {
11
- return startVaultCore({ ...opts, platform: nodePlatform(dir) })
10
+ /**
11
+ * `platform` suma o cambia piezas de la del binario: la Lambda (`lambda/session.mjs`) pone
12
+ * la URL que la despierta y un cliente del proxio que le dice cuándo hay trabajo.
13
+ */
14
+ export async function startVault ({ dir = dataDir(), platform = null, ...opts } = {}) {
15
+ return startVaultCore({ ...opts, platform: { ...nodePlatform(dir), ...(platform || {}) } })
12
16
  }
13
17
 
14
18
  export default { startVault }