@dotrino/vault 0.61.0 → 0.62.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.
@@ -218,7 +218,13 @@ async function cmdCheck () {
218
218
  // `--public` también aquí: ver QUÉ hay no debería costarle una aprobación al dueño. Sin
219
219
  // él se piden todas, que es lo que hace falta para comprobar de verdad un arranque.
220
220
  const publicOnly = argv.includes('--public')
221
- const secrets = await fetchSecrets({ dir: serviceDir(etiquetaBoveda(), ns), ns, publicOnly })
221
+ // Y SE DICE QUE ESTÁ ESPERANDO. Sin este aviso `check` se queda mudo cinco minutos —el
222
+ // plazo de la aprobación— y desde fuera eso es un cuelgue: ni una línea que diga que la
223
+ // bóveda contestó, que lo que falta es un permiso, ni cuánto va a esperar. `run` sí lo
224
+ // decía; `check`, que es el comando de DIAGNÓSTICO, era el único que no. Le pasó al dueño
225
+ // el 2026-09-05 con un cajón cuyo servicio no tenía a nadie que pudiera aprobarle.
226
+ const onPending = () => console.error('[dotrino-env] la bóveda pide aprobación: apruébalo en un aparato con «aprueba» (hasta 5 min)')
227
+ const secrets = await fetchSecrets({ dir: serviceDir(etiquetaBoveda(), ns), ns, publicOnly, onPending })
222
228
  const keys = Object.keys(secrets)
223
229
  console.log('ns "%s": %d %s%s', ns, keys.length,
224
230
  publicOnly ? 'variable(s) pública(s)' : 'secreto(s)', keys.length ? ':' : '')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vault",
3
- "version": "0.61.0",
3
+ "version": "0.62.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.17.0"
54
+ "@dotrino/proxy-client": ">=0.18.0"
55
55
  },
56
56
  "license": "MIT",
57
57
  "repository": {
package/src/admin.js CHANGED
@@ -61,7 +61,11 @@ export const ADMIN_OPS = Object.freeze([
61
61
  // documentada en `buildSealedVar` y la consola la llamaba, pero no existía — así que
62
62
  // crear una variable desde la consola contestaba «admin: invalid operation». No expone
63
63
  // nada nuevo: son llaves PÚBLICAS, las mismas que ya viajan en el acta.
64
- 'vars', 'var.set', 'var.setMany', 'var.recipients'
64
+ 'vars', 'var.set', 'var.setMany', 'var.recipients',
65
+ // ABRIR LA BÓVEDA A DISTANCIA (`docs/abrir-a-distancia.md`). Dos pasos: `unlock.begin`
66
+ // pide una llave efímera y los parámetros del molino; `unlock` trae la derivada dentro
67
+ // de un sobre que solo esta bóveda puede abrir. No hay una tercera.
68
+ 'unlock.begin', 'unlock'
65
69
  ])
66
70
 
67
71
  /**
@@ -90,7 +94,13 @@ export const ADMIN_OPS = Object.freeze([
90
94
  * Se dejan las de LEER a propósito: si se cortaran todas, la consola no podría ni
91
95
  * enterarse de por qué no funciona y parecería rota. Así puede decir «está cerrada».
92
96
  */
93
- export const ADMIN_OPS_WHILE_LOCKED = Object.freeze(['audit', 'vars', 'var.set', 'var.setMany', 'var.recipients'])
97
+ export const ADMIN_OPS_WHILE_LOCKED = Object.freeze([
98
+ 'audit', 'vars', 'var.set', 'var.setMany', 'var.recipients',
99
+ // Las dos de abrir, obviamente: son las únicas que solo tienen sentido con el candado
100
+ // echado. Que estén aquí NO las deja pasar sin más — siguen exigiendo cert, acta, nonce
101
+ // y frescura como todas, y además la contraseña dentro del sobre.
102
+ 'unlock.begin', 'unlock'
103
+ ])
94
104
 
95
105
  /** Cuánto se recuerda un nonce ya usado (el doble de la ventana de frescura). */
96
106
  export const ADMIN_NONCE_TTL_MS = 10 * 60 * 1000
@@ -133,6 +143,10 @@ export function authorBody (owner, key, e, ts) {
133
143
  export function createAdminDesk ({
134
144
  desk, verify, readActivity = () => [], deviceIdOf, vars = null,
135
145
  notify = async () => {}, audit = () => {}, isLocked = () => false,
146
+ // ABRIR A DISTANCIA. Va inyectado —como `vars`— porque abrir necesita cripto y disco, y
147
+ // este módulo es puro a propósito. `null` en una bóveda que no lo soporte: entonces la
148
+ // operación existe y contesta que no, en vez de romper con un TypeError.
149
+ unlockDesk = null,
136
150
  now = () => Date.now()
137
151
  }) {
138
152
  const ops = new Set(ADMIN_OPS)
@@ -193,6 +207,17 @@ export function createAdminDesk ({
193
207
 
194
208
  try {
195
209
 
210
+ // ABRIR A DISTANCIA. La bóveda hace el trabajo; aquí solo se enruta, porque este
211
+ // módulo es puro —sin cripto, sin disco— y abrir necesita las dos cosas.
212
+ if (data.op === 'unlock.begin') {
213
+ if (!unlockDesk) return { ok: false, error: 'admin: this vault cannot be opened remotely (update it)' }
214
+ return { ok: true, result: await unlockDesk.begin({ by, device: chk.device }) }
215
+ }
216
+ if (data.op === 'unlock') {
217
+ if (!unlockDesk) return { ok: false, error: 'admin: this vault cannot be opened remotely (update it)' }
218
+ return await unlockDesk.open({ nonce: data.nonce, enc: data.enc, by, device: chk.device })
219
+ }
220
+
196
221
  if (data.op === 'audit') {
197
222
  const limit = Math.min(Math.max(Number(data.limit) || 100, 1), AUDIT_MAX)
198
223
  return { ok: true, result: { entries: readActivity(limit) } }
package/src/index.js CHANGED
@@ -97,9 +97,8 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
97
97
  const selfCert = await getSelfCert()
98
98
  const identify = async () => {
99
99
  if (!client.token) return
100
- const data = { op: 'identify', publickey: iss, token: client.token, ts: Date.now() }
101
- const { signature } = await identity.signData(data)
102
- await client.identify({ data, signature, cert: selfCert })
100
+ // El sobre lo arma el pilar (`identifyAs`), que le pone el destinatario.
101
+ await client.identifyAs({ publickey: iss, sign: (d) => identity.signData(d), cert: selfCert })
103
102
  }
104
103
  await identify()
105
104
  client.on('token', () => identify().catch(() => {}))
package/src/localdesk.js CHANGED
@@ -85,7 +85,15 @@ export async function connectLocal ({ masterId, timeoutMs = 3000 } = {}) {
85
85
  // El mostrador local no pide identificarse: la puerta la abre el certificado que va
86
86
  // dentro de cada mensaje, igual que por el proxio. Identificarse ahí sirve para ser
87
87
  // DIRECCIONABLE, y aquí no hay a quién direccionar.
88
+ //
89
+ // LAS DOS FORMAS, porque quien llama no sabe por dónde salió. `freshClient` devuelve
90
+ // esto o un `WebSocketProxyClient` según haya socket o no, y el de arriba llama al
91
+ // método sin más: si aquí falta el que el pilar usa ahora, el escalón 1 del transporte
92
+ // —el más rápido, y el que corre en la máquina del dueño— muere con «client.identifyAs
93
+ // is not a function» mientras por el proxio todo va bien. Que sea un no-op no lo hace
94
+ // opcional: es la forma del cliente, y tiene que estar entera.
88
95
  identify: async () => ({ ok: true }),
96
+ identifyAs: async () => ({ ok: true }),
89
97
  on: (ev, fn) => {
90
98
  if (ev !== 'message') return () => {}
91
99
  oyentes.add(fn)
package/src/service.js CHANGED
@@ -199,9 +199,8 @@ async function freshClient (proxyUrl, connectTimeoutMs = 20000, master = null) {
199
199
 
200
200
  /** Identifica la conexión bajo la pubkey del servicio (para ser direccionable). */
201
201
  async function identifyAsService (client, device) {
202
- const data = { op: 'identify', publickey: device.publickey, token: client.token, ts: Date.now() }
203
- const { signature } = await signWithDevice({ privateJwk: device.privateJwk, data })
204
- await client.identify({ data, signature })
202
+ // El sobre lo arma el pilar (`identifyAs`), que le pone el destinatario.
203
+ await client.identifyAs({ publickey: device.publickey, sign: (d) => signWithDevice({ privateJwk: device.privateJwk, data: d }) })
205
204
  }
206
205
 
207
206
  /** Cuánto espera un agente a que alguien apruebe (el pedido vive 5 min en la bóveda). */