@dotrino/vault 0.60.4 → 0.62.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/LICENSE +0 -0
- package/README.md +0 -0
- package/bin/dotrino-env.js +7 -1
- package/package.json +1 -1
- package/src/admin.js +27 -2
- package/src/atrest.js +0 -0
- package/src/config.js +0 -0
- package/src/enroll.js +0 -0
- package/src/env.js +0 -0
- package/src/envtext.js +0 -0
- package/src/index.js +2 -3
- package/src/invite.js +0 -0
- package/src/kek.js +0 -0
- package/src/localdesk.js +8 -0
- package/src/lock.js +0 -0
- package/src/protocol.js +15 -0
- package/src/revocation.js +0 -0
- package/src/sealed.js +0 -0
- package/src/service.js +25 -5
- package/src/sshAgent.js +0 -0
- package/src/sshKeys.js +0 -0
- package/src/version.js +30 -0
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
File without changes
|
package/bin/dotrino-env.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
3
|
+
"version": "0.62.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",
|
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([
|
|
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/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
|
@@ -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
|
-
|
|
101
|
-
|
|
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/invite.js
CHANGED
|
File without changes
|
package/src/kek.js
CHANGED
|
File without changes
|
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/lock.js
CHANGED
|
File without changes
|
package/src/protocol.js
CHANGED
|
@@ -128,3 +128,18 @@ export const isValidSecretsNs = (ns) => typeof ns === 'string' && /^[a-z0-9-]{1,
|
|
|
128
128
|
* remota y el lector de `.env`, y tres copias de una regla son tres reglas.
|
|
129
129
|
*/
|
|
130
130
|
export const isValidVarKey = (key) => typeof key === 'string' && /^[A-Z0-9_]{1,64}$/.test(key)
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* LA VERSIÓN DEL CABLE (CONVENCIONES §14, `@dotrino/compat`).
|
|
134
|
+
*
|
|
135
|
+
* Sube **solo cuando cambia el protocolo**, no con cada release: es lo que decide *si dos
|
|
136
|
+
* piezas pueden hablar*. La `version` del paquete decide otra cosa —si esa build concreta
|
|
137
|
+
* está rota— y por eso hacen falta las dos.
|
|
138
|
+
*
|
|
139
|
+
* Empieza en 1 hoy, con todo lo anterior sin declarar nada: a quien calla se le atiende
|
|
140
|
+
* hasta que cierre la ventana de migración del pilar, y después no.
|
|
141
|
+
*/
|
|
142
|
+
export const VAULT_PROTOCOL = 1
|
|
143
|
+
|
|
144
|
+
/** Los protocolos que esta build entiende. Al añadir el 2, aquí van `[1, 2]`. */
|
|
145
|
+
export const VAULT_SPEAKS = [1]
|
package/src/revocation.js
CHANGED
|
File without changes
|
package/src/sealed.js
CHANGED
|
File without changes
|
package/src/service.js
CHANGED
|
@@ -26,7 +26,8 @@ import {
|
|
|
26
26
|
verifyDeviceSig, makePairingCode, commitCode, pubkeyId } from '@dotrino/identity/capabilities'
|
|
27
27
|
import { openWrap, wrapForMember, decryptWithCek } from '@dotrino/identity/content'
|
|
28
28
|
import { verifyActa, sealKeyAt, sealersOf, memberCan } from '@dotrino/identity/acta'
|
|
29
|
-
import { MSG, secretsScope, isValidSecretsNs } from './protocol.js'
|
|
29
|
+
import { MSG, secretsScope, isValidSecretsNs, VAULT_PROTOCOL, VAULT_SPEAKS } from './protocol.js'
|
|
30
|
+
import { PKG_VERSION } from './version.js'
|
|
30
31
|
import { makeEphemeralKey, openSealed } from './sealed.js'
|
|
31
32
|
import { parseInvite } from './invite.js'
|
|
32
33
|
import { atRestFor } from './atrest.js'
|
|
@@ -35,6 +36,26 @@ import { connectLocal, hasLocalVault } from './localdesk.js'
|
|
|
35
36
|
const IDENTITY_FILE = 'service-identity.json'
|
|
36
37
|
const FRESH_WINDOW_MS = 5 * 60 * 1000
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* QUÉ SOMOS, dentro de lo que ya va firmado (CONVENCIONES §14).
|
|
41
|
+
*
|
|
42
|
+
* Va DENTRO de `data` y no en el envoltorio a propósito: `data` lo firma la llave del
|
|
43
|
+
* aparato, así que lo que la bóveda apunta y enseña es lo que ese aparato dijo **con su
|
|
44
|
+
* llave**. Un inventario de versiones que cualquiera puede escribir no es un inventario.
|
|
45
|
+
*
|
|
46
|
+
* `PRODUCT` es el cliente de la bóveda —el que se instala con `@dotrino/env`—, que es
|
|
47
|
+
* justo la pieza que se quedaba atrás sin que nadie se enterara: pedía `^0.33.2` con la
|
|
48
|
+
* librería en 0.60 y enrolar contestaba `invalid cert: no-acta`, que es verdad y no es la
|
|
49
|
+
* causa.
|
|
50
|
+
*/
|
|
51
|
+
const PRODUCT = 'vault'
|
|
52
|
+
export const MY_VERSION = {
|
|
53
|
+
product: PRODUCT,
|
|
54
|
+
version: PKG_VERSION,
|
|
55
|
+
protocol: VAULT_PROTOCOL,
|
|
56
|
+
speaks: VAULT_SPEAKS
|
|
57
|
+
}
|
|
58
|
+
|
|
38
59
|
let _globalsInstalled = false
|
|
39
60
|
function installNodeGlobals () {
|
|
40
61
|
if (_globalsInstalled) return
|
|
@@ -178,9 +199,8 @@ async function freshClient (proxyUrl, connectTimeoutMs = 20000, master = null) {
|
|
|
178
199
|
|
|
179
200
|
/** Identifica la conexión bajo la pubkey del servicio (para ser direccionable). */
|
|
180
201
|
async function identifyAsService (client, device) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
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 }) })
|
|
184
204
|
}
|
|
185
205
|
|
|
186
206
|
/** Cuánto espera un agente a que alguien apruebe (el pedido vive 5 min en la bóveda). */
|
|
@@ -593,7 +613,7 @@ export async function fetchSecrets ({ dir, ns, proxyUrl, masterPubkey, device, c
|
|
|
593
613
|
const eph = await makeEphemeralKey()
|
|
594
614
|
// `publicOnly` VIAJA FIRMADO (va dentro de `data`), así que nadie lo puede quitar ni
|
|
595
615
|
// poner por el camino. Quien decide qué se manda es la bóveda; esto solo es la petición.
|
|
596
|
-
const data = { op: 'secrets', ns, ek: eph.ek, publickey: device.publickey, ts: Date.now(), ...(publicOnly ? { publicOnly: true } : {}) }
|
|
616
|
+
const data = { op: 'secrets', ns, ek: eph.ek, publickey: device.publickey, ts: Date.now(), v: MY_VERSION, ...(publicOnly ? { publicOnly: true } : {}) }
|
|
597
617
|
const { signature } = await signWithDevice({ privateJwk: device.privateJwk, data })
|
|
598
618
|
// A QUIÉN SE LE PREGUNTA: primero a la bóveda, y si no contesta, a los REPLICADORES
|
|
599
619
|
// que el acta nombraba la última vez que la vimos (`docs/replicas.md` §8: «el servicio
|
package/src/sshAgent.js
CHANGED
|
File without changes
|
package/src/sshKeys.js
CHANGED
|
File without changes
|
package/src/version.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* version.js — la versión de `@dotrino/vault` (el CLIENTE, no el daemon).
|
|
3
|
+
*
|
|
4
|
+
* Existe porque el cliente tiene que poder decir qué versión es (CONVENCIONES §14), y esta
|
|
5
|
+
* es justo la pieza que se quedaba atrás sin que nadie se enterara: `@dotrino/env` pedía
|
|
6
|
+
* `^0.33.2` con la librería en 0.60, y enrolar contestaba `invalid cert: no-acta` — verdad,
|
|
7
|
+
* y no la causa.
|
|
8
|
+
*
|
|
9
|
+
* Se lee del `package.json` que viaja con el paquete. Si no se puede leer se dice `0.0.0`,
|
|
10
|
+
* que es un número imposible y por tanto legible como «no lo sé» — no se inventa uno
|
|
11
|
+
* plausible, que sería peor que no decir nada.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
import { dirname, join } from 'node:path'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
|
|
17
|
+
function leer () {
|
|
18
|
+
try {
|
|
19
|
+
const aqui = dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
for (const p of [join(aqui, '..', 'package.json'), join(aqui, '..', '..', 'package.json')]) {
|
|
21
|
+
try {
|
|
22
|
+
const v = JSON.parse(fs.readFileSync(p, 'utf8'))
|
|
23
|
+
if (v?.name === '@dotrino/vault' && typeof v.version === 'string') return v.version
|
|
24
|
+
} catch (_) {}
|
|
25
|
+
}
|
|
26
|
+
} catch (_) {}
|
|
27
|
+
return '0.0.0'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const PKG_VERSION = leer()
|