@dotrino/vaultd 0.106.2 → 0.107.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/lib/src/protocol.js +15 -0
- package/lib/src/service.js +23 -2
- package/lib/src/version.js +30 -0
- package/package.json +3 -1
- package/src/ctl.js +12 -0
- package/src/vault.js +86 -5
package/lib/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/lib/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
|
|
@@ -593,7 +614,7 @@ export async function fetchSecrets ({ dir, ns, proxyUrl, masterPubkey, device, c
|
|
|
593
614
|
const eph = await makeEphemeralKey()
|
|
594
615
|
// `publicOnly` VIAJA FIRMADO (va dentro de `data`), así que nadie lo puede quitar ni
|
|
595
616
|
// 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 } : {}) }
|
|
617
|
+
const data = { op: 'secrets', ns, ek: eph.ek, publickey: device.publickey, ts: Date.now(), v: MY_VERSION, ...(publicOnly ? { publicOnly: true } : {}) }
|
|
597
618
|
const { signature } = await signWithDevice({ privateJwk: device.privateJwk, data })
|
|
598
619
|
// A QUIÉN SE LE PREGUNTA: primero a la bóveda, y si no contesta, a los REPLICADORES
|
|
599
620
|
// que el acta nombraba la última vez que la vimos (`docs/replicas.md` §8: «el servicio
|
|
@@ -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()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.107.0",
|
|
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": {
|
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
"node": ">=20"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"@dotrino/compat": "0.4.0",
|
|
24
25
|
"@dotrino/identity": "0.79.0",
|
|
25
26
|
"@dotrino/passmanager": "^0.10.1",
|
|
26
27
|
"@dotrino/proxy-client": "0.15.0",
|
|
28
|
+
"@dotrino/roadmap": "0.1.0",
|
|
27
29
|
"ws": "^8.18.0"
|
|
28
30
|
},
|
|
29
31
|
"license": "MIT",
|
package/src/ctl.js
CHANGED
|
@@ -595,6 +595,18 @@ async function cmdMembers () {
|
|
|
595
595
|
// selladas a esa llave. Se dice aquí, junto a él, porque es el único sitio donde
|
|
596
596
|
// se mira quién es quién — y en la lista de variables ya seria tarde.
|
|
597
597
|
if (m.cn && !m.canSeal) console.log(' %ssin llave de cifrado: NO puede leer sus variables%s', R, Z)
|
|
598
|
+
// QUÉ CORRE, y si cuadra con esta bóveda (CONVENCIONES §14). Sin esto una
|
|
599
|
+
// incompatibilidad de versiones se ve como que ese aparato «no responde», que es el
|
|
600
|
+
// fallo más caro que ha tenido el ecosistema. No bloquea nada: se enseña.
|
|
601
|
+
if (m.running) {
|
|
602
|
+
const v = `${m.running.product} ${m.running.version}`
|
|
603
|
+
if (m.running.compatible) console.log(' %scorre %s%s', D, v, Z)
|
|
604
|
+
else console.log(' %s⚠ corre %s · %s%s', R, v, m.running.reason, Z)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (record.vault) {
|
|
608
|
+
console.log('\n %sEsta bóveda corre %s %s (protocolo %d)%s',
|
|
609
|
+
D, record.vault.product, record.vault.version, record.vault.protocol, Z)
|
|
598
610
|
}
|
|
599
611
|
console.log('\n Cambiar permisos: dotrino-vault caps <ID> +firma | -firma | +guarda | -guarda | +lee | -lee | +administra | +aprueba | +contrasenas | +sella | +desatendido | +replica')
|
|
600
612
|
console.log(' «Permiso»: ese aparato solo recibe claves privadas cuando lo apruebas desde un aparato con «aprueba» (en cada arranque).')
|
package/src/vault.js
CHANGED
|
@@ -14,7 +14,7 @@ import fs from 'node:fs'
|
|
|
14
14
|
import nodeCrypto from 'node:crypto'
|
|
15
15
|
import path from 'node:path'
|
|
16
16
|
import { Identity } from '@dotrino/identity/node'
|
|
17
|
-
import { verifyChain, pubkeyId, verifyDeviceSig } from '@dotrino/identity/capabilities'
|
|
17
|
+
import { verifyChain as verifyChainRaw, pubkeyId, verifyDeviceSig } from '@dotrino/identity/capabilities'
|
|
18
18
|
import * as Acta from '@dotrino/identity/acta'
|
|
19
19
|
import { createEnrollDesk, deviceIdOf, DEVICE_TTL_MS } from '../lib/src/enroll.js'
|
|
20
20
|
import { createAdminDesk, authorBody } from '../lib/src/admin.js'
|
|
@@ -27,6 +27,9 @@ import { assertKeyOwnsDir } from './keyowner.js'
|
|
|
27
27
|
import { openStore } from './store.js'
|
|
28
28
|
import { openThreadStore, STORE_READ_METHODS, PROFILE_EDIT_METHODS } from './threadStore.js'
|
|
29
29
|
import { openSecretsStore, assertVar, RECOVERY as RECOVERY_WRAP, PROFILE_OWNER } from './secretsStore.js'
|
|
30
|
+
import { VERSION } from './version.js'
|
|
31
|
+
import { declare as compatDeclare, check as compatCheck, annotate as compatAnnotate } from '@dotrino/compat'
|
|
32
|
+
import { loadManifest, brokenOf } from '@dotrino/roadmap'
|
|
30
33
|
// `PENDING_TTL_MS` se usa abajo, al esperar la firma del aprobador: sin importarlo, esa
|
|
31
34
|
// espera reventaba con un ReferenceError y la aprobación del mostrador de contraseñas no
|
|
32
35
|
// llegaba a existir. Solo se veía por ese camino —el único que lo usa—, y no había prueba
|
|
@@ -40,7 +43,7 @@ import { localSocketPath, socketDir } from '../lib/src/localdesk.js'
|
|
|
40
43
|
import { dataDir, ensureDir } from './paths.js'
|
|
41
44
|
import net from 'node:net'
|
|
42
45
|
import { atRestFor, kekFor, migrateFile, encryptText, decryptText } from './atrest.js'
|
|
43
|
-
import { MSG, SCOPE, secretsScope, isValidSecretsNs } from './protocol.js'
|
|
46
|
+
import { MSG, SCOPE, secretsScope, isValidSecretsNs, VAULT_PROTOCOL, VAULT_SPEAKS } from './protocol.js'
|
|
44
47
|
|
|
45
48
|
/**
|
|
46
49
|
* Tope de una respuesta de la bóveda por el transporte. El proxio (`PROXY_MAX_FRAME_BYTES`)
|
|
@@ -333,9 +336,67 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
333
336
|
*/
|
|
334
337
|
const canales = new Map()
|
|
335
338
|
|
|
339
|
+
/**
|
|
340
|
+
* QUÉ SOMOS, Y QUÉ CORRE CADA APARATO (CONVENCIONES §14, `@dotrino/compat`).
|
|
341
|
+
*
|
|
342
|
+
* El agujero que esto tapa: una incompatibilidad de versiones se manifestaba como
|
|
343
|
+
* SILENCIO. El que llamaba no sabía si estábamos apagados, ocupados o hablando otro
|
|
344
|
+
* idioma, así que reintentaba para siempre — y aquí no se sabía que nos hablaban.
|
|
345
|
+
*
|
|
346
|
+
* **Informa, no bloquea** (dueño, 2026-09-04): el dictamen se enseña y se le pega a los
|
|
347
|
+
* errores, y no hay ni un `if` que deje de atender por él. Si algún día se decide
|
|
348
|
+
* bloquear, se decide arriba y se escribe aquí a propósito.
|
|
349
|
+
*
|
|
350
|
+
* La lista de rotas sale del registro común (`@dotrino/roadmap`), que viaja DENTRO de
|
|
351
|
+
* esta versión: la compatibilidad es política del código, no de la cuenta.
|
|
352
|
+
*/
|
|
353
|
+
const MI_VERSION = compatDeclare({
|
|
354
|
+
product: 'vaultd', version: VERSION, protocol: VAULT_PROTOCOL, speaks: VAULT_SPEAKS
|
|
355
|
+
})
|
|
356
|
+
let ROTAS = []
|
|
357
|
+
try { ROTAS = brokenOf(loadManifest()) } catch (e) { log('[vault] no compatibility registry: ' + e.message) }
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Lo que dice correr cada aparato, por llave. Se apunta de lo que viene FIRMADO
|
|
361
|
+
* (`p.data.v`, dentro del sobre) y no del envoltorio: una versión que cualquiera puede
|
|
362
|
+
* escribir no vale para enseñarla como si fuera un dato.
|
|
363
|
+
*/
|
|
364
|
+
const versiones = new Map()
|
|
365
|
+
const apuntarVersion = (pub, v) => {
|
|
366
|
+
if (!pub || !v) return
|
|
367
|
+
const dictamen = compatCheck({ mine: MI_VERSION, theirs: v, broken: ROTAS })
|
|
368
|
+
versiones.set(pub, { ...v, at: Date.now(), compatible: dictamen.compatible, reason: dictamen.reason })
|
|
369
|
+
if (!dictamen.compatible) log(`[vault] ${v.product} ${v.version}: ${dictamen.reason}`)
|
|
370
|
+
return dictamen
|
|
371
|
+
}
|
|
372
|
+
/** El dictamen de un aparato, para enseñarlo o para pegárselo a un error. */
|
|
373
|
+
const versionDe = (pub) => versiones.get(pub) || null
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* LA VERSIÓN SE APUNTA DONDE YA SE COMPRUEBA LA FIRMA, y por eso esto envuelve a
|
|
377
|
+
* `verifyChain` en vez de mirar el envoltorio del mensaje: `data` va firmado, así que
|
|
378
|
+
* lo que se apunta es lo que ese aparato dijo **con su llave**, no lo que cualquiera
|
|
379
|
+
* pueda escribir por fuera. Un inventario de versiones que se puede falsificar no es un
|
|
380
|
+
* inventario, es una pantalla bonita.
|
|
381
|
+
*
|
|
382
|
+
* Va envuelto y no repetido en los ocho mostradores porque una comprobación copiada
|
|
383
|
+
* ocho veces son siete sitios donde olvidarla.
|
|
384
|
+
*/
|
|
385
|
+
const verifyChain = async (args) => {
|
|
386
|
+
const chk = await verifyChainRaw(args)
|
|
387
|
+
if (chk.ok && args?.data?.v) apuntarVersion(chk.device, args.data.v)
|
|
388
|
+
return chk
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
336
392
|
const reply = (to, obj) => {
|
|
337
393
|
try {
|
|
338
394
|
const enviar = canales.get(to) || ((o) => client.send(to, o))
|
|
395
|
+
// ANUNCIAMOS EN CADA RESPUESTA: el otro lado decide con esto, y decide él porque es
|
|
396
|
+
// el que puede tener la lista al día (§14). Va en el envoltorio y no dentro de un
|
|
397
|
+
// cuerpo firmado a propósito — es un dato para enseñar y para explicar un fallo, no
|
|
398
|
+
// una credencial: nada se autoriza con él.
|
|
399
|
+
obj = { ...obj, v: MI_VERSION }
|
|
339
400
|
const bytes = Buffer.byteLength(JSON.stringify(obj))
|
|
340
401
|
if (bytes > MAX_REPLY_BYTES) {
|
|
341
402
|
log(`[vault] the reply to ${obj?.type} does not fit (${bytes} bytes > ${MAX_REPLY_BYTES}): sending an error instead of killing the connection`)
|
|
@@ -889,10 +950,15 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
889
950
|
// `sub` (pubkey completa) va incluida: es la DIRECCIÓN de cada dispositivo en el
|
|
890
951
|
// proxy → permite a las apps AUTODESCUBRIR tus máquinas (p. ej. la terminal
|
|
891
952
|
// lista tus agentes sin pegar nada). Solo la ve quien presenta un cert tuyo válido.
|
|
953
|
+
// QUÉ CORRE CADA UNO, y si cuadra con lo que corremos aquí (CONVENCIONES §14). Va con
|
|
954
|
+
// la lista porque es donde se mira: el dueño lo pidió en el administrador Y en la
|
|
955
|
+
// lista de dispositivos de la bóveda. `null` significa «todavía no ha hablado desde
|
|
956
|
+
// que arrancamos», que es distinto de «no declara» — y se dice distinto.
|
|
892
957
|
const devices = await Promise.all(issued.map(async (x) => ({
|
|
893
|
-
deviceId: x.sub ? await deviceIdOf(x.sub) : null, sub: x.sub || null, label: x.label || '', scope: x.scope, exp: x.exp, nonce: x.nonce
|
|
958
|
+
deviceId: x.sub ? await deviceIdOf(x.sub) : null, sub: x.sub || null, label: x.label || '', scope: x.scope, exp: x.exp, nonce: x.nonce,
|
|
959
|
+
running: x.sub ? versionDe(x.sub) : null
|
|
894
960
|
})))
|
|
895
|
-
reply(from, fitChain({ type: MSG.DEVICES_RESULT, devices, revoked, acta: record, chain }))
|
|
961
|
+
reply(from, fitChain({ type: MSG.DEVICES_RESULT, devices, revoked, acta: record, chain, vault: MI_VERSION }))
|
|
896
962
|
}
|
|
897
963
|
|
|
898
964
|
/**
|
|
@@ -3038,7 +3104,22 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
3038
3104
|
listDevices: () => identity.listDelegations(),
|
|
3039
3105
|
// Acta del perfil (quién es del perfil y qué puede cada uno): lo que muestran
|
|
3040
3106
|
// `dotrino-vault members` y la consola de vault.dotrino.com.
|
|
3041
|
-
|
|
3107
|
+
/**
|
|
3108
|
+
* Los miembros del acta, y QUÉ CORRE CADA UNO (CONVENCIONES §14). La versión no está en
|
|
3109
|
+
* el acta —el acta dice quién puede qué, no qué build es— así que se le pega aquí, que
|
|
3110
|
+
* es por donde salen `members` y la consola. El dueño lo pidió en los dos sitios.
|
|
3111
|
+
*
|
|
3112
|
+
* `running: null` es «todavía no ha hablado desde que arrancamos», que no es lo mismo
|
|
3113
|
+
* que «no declara»: lo segundo trae un dictamen con `code: 'undeclared'`.
|
|
3114
|
+
*/
|
|
3115
|
+
profileMembers: async () => {
|
|
3116
|
+
const r = await identity.profileMembers()
|
|
3117
|
+
return {
|
|
3118
|
+
...r,
|
|
3119
|
+
vault: MI_VERSION,
|
|
3120
|
+
members: (r?.members || []).map((m) => ({ ...m, running: versionDe(m.pub) }))
|
|
3121
|
+
}
|
|
3122
|
+
},
|
|
3042
3123
|
// Los namespaces que quedaron a deber una rotación (se fue un miembro y no se pudo
|
|
3043
3124
|
// rotar su llave). Lo enseñan `secret list` y la consola: si no se ve, no se salda.
|
|
3044
3125
|
rotationsDue,
|