@dotrino/vault 0.46.0 → 0.47.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/bin/dotrino-env.js +38 -29
- package/package.json +1 -1
- package/src/env.js +25 -21
package/bin/dotrino-env.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* dotrino-env — CLI del "dotenv contra el vault".
|
|
4
4
|
*
|
|
5
|
-
* dotrino-env enroll --
|
|
5
|
+
* dotrino-env enroll --vault <b> --ns <ns> [--qr <inv>] enrola ESTA máquina (una vez)
|
|
6
6
|
* dotrino-env status qué hay enrolado aquí
|
|
7
7
|
* dotrino-env check [--ns <ns>] pide los secretos y lista sus NOMBRES (nunca valores)
|
|
8
8
|
* dotrino-env run [--ns <ns>] -- <cmd> [args…] corre un comando con los secretos en su entorno
|
|
9
9
|
*
|
|
10
10
|
* El enrolamiento es el registro del cliente contra el vault del dueño:
|
|
11
11
|
* 1. en el vault: dotrino-vault pair --service <ns> (invitación con scope SOLO vault:secrets:<ns>)
|
|
12
|
-
* 2. aquí: dotrino-env enroll --
|
|
12
|
+
* 2. aquí: dotrino-env enroll --vault <b> --ns <ns> (pegas la invitación; se MUESTRA un código)
|
|
13
13
|
* 3. en el vault: dotrino-vault approve <código> (lo tipeas leyéndolo de esta pantalla)
|
|
14
14
|
*/
|
|
15
15
|
import fs from 'node:fs'
|
|
@@ -17,7 +17,7 @@ import path from 'node:path'
|
|
|
17
17
|
import readline from 'node:readline/promises'
|
|
18
18
|
import { spawn } from 'node:child_process'
|
|
19
19
|
import { enrollService, readServiceIdentity, fetchSecrets } from '../src/service.js'
|
|
20
|
-
import { loadEnv, serviceDir, serviceRoot, listEnrolled, resolveNs,
|
|
20
|
+
import { loadEnv, serviceDir, serviceRoot, listEnrolled, resolveNs, resolveVaultLabel, isValidVaultLabel } from '../src/env.js'
|
|
21
21
|
// Se usaba sin importarlo: `enroll` —el comando principal— moría con
|
|
22
22
|
// «sharedParseInvite is not defined» en cuanto tocaba una invitación. Nunca
|
|
23
23
|
// llegó a funcionar, y por eso el único servicio enrolado del ecosistema (el
|
|
@@ -30,14 +30,14 @@ 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 --
|
|
33
|
+
enroll --vault <bóveda> --ns <ns> [--code <invitación>] [--dir <dir>]
|
|
34
34
|
Registra ESTE servicio contra el vault (una sola vez).
|
|
35
35
|
Antes, en el vault: dotrino-vault pair --service <ns>
|
|
36
36
|
Si no pasas --code (alias --qr), se pide por consola (también acepta stdin).
|
|
37
37
|
|
|
38
38
|
status servicios enrolados en esta máquina
|
|
39
39
|
check [--ns <ns>] pide los secretos al vault y lista sus NOMBRES (nunca los valores)
|
|
40
|
-
run --
|
|
40
|
+
run --vault <bóveda> [--ns <ns>] -- <cmd> [args…]
|
|
41
41
|
ssh-agent [--ns <ns>] [--socket <ruta>]
|
|
42
42
|
agente SSH con las llaves (SSH_KEY_* del cajón) solo en memoria;
|
|
43
43
|
pide el cajón al arrancar. Pon SSH_AUTH_SOCK en la ruta que imprime.
|
|
@@ -50,7 +50,7 @@ En tu código:
|
|
|
50
50
|
El vault MANDA: sus valores pisan los del .env y los del entorno. Para una
|
|
51
51
|
corrida suelta sin que pise nada: DOTRINO_ENV_OVERRIDE=0
|
|
52
52
|
|
|
53
|
-
Entorno: DOTRINO_NS ·
|
|
53
|
+
Entorno: DOTRINO_NS · DOTRINO_ENV_VAULT · DOTRINO_ENV_DIR · DOTRINO_ENV_HOME · DOTRINO_ENV_QUIET
|
|
54
54
|
DOTRINO_ENV_OVERRIDE`)
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -74,30 +74,33 @@ async function readInvite () {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* DE QUÉ BÓVEDA VINO, obligatorio. Etiqueta de ESTA máquina para agrupar sus cajones:
|
|
78
|
+
* `~/.dotrino/service/<bóveda>/<ns>`. No viaja y no se compara con nada — solo evita que
|
|
79
|
+
* dos bóvedas con un cajón del mismo nombre se pisen el directorio, que antes dejaba al
|
|
80
|
+
* primero inservible.
|
|
81
|
+
*
|
|
82
|
+
* `--vault` y no `--profile`: `dotrino-vault --profile` ya significa otra cosa y los dos
|
|
83
|
+
* comandos se usan seguidos.
|
|
81
84
|
*/
|
|
82
|
-
function
|
|
83
|
-
const
|
|
84
|
-
if (!
|
|
85
|
-
console.error('falta --
|
|
85
|
+
function etiquetaBoveda () {
|
|
86
|
+
const v = resolveVaultLabel(flag('vault'))
|
|
87
|
+
if (!v) {
|
|
88
|
+
console.error('falta --vault <nombre> (o DOTRINO_ENV_VAULT)')
|
|
86
89
|
console.error('')
|
|
87
|
-
console.error('
|
|
88
|
-
console.error(' ~/.dotrino/service/<
|
|
90
|
+
console.error(' Dice DE QUÉ BÓVEDA es este cajón. Es una etiqueta tuya, de esta máquina:')
|
|
91
|
+
console.error(' ~/.dotrino/service/<bóveda>/<ns>')
|
|
89
92
|
console.error(' Sirve para que dos bóvedas puedan tener un cajón llamado igual.')
|
|
90
93
|
process.exit(2)
|
|
91
94
|
}
|
|
92
|
-
if (!
|
|
93
|
-
return
|
|
95
|
+
if (!isValidVaultLabel(v)) { console.error('etiqueta inválida: usa [a-z0-9-]{1,32}'); process.exit(2) }
|
|
96
|
+
return v
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
async function cmdEnroll () {
|
|
97
100
|
const ns = flag('ns')
|
|
98
101
|
if (!ns) { console.error('falta --ns <ns> (el mismo del `dotrino-vault pair --service <ns>`)'); process.exit(2) }
|
|
99
|
-
const perfil =
|
|
100
|
-
const dir = flag('dir') || serviceDir(
|
|
102
|
+
const perfil = etiquetaBoveda()
|
|
103
|
+
const dir = flag('dir') || serviceDir(bov, ns)
|
|
101
104
|
const qr = parseInvite((flag('qr') || flag('code')) || await readInvite())
|
|
102
105
|
|
|
103
106
|
console.log('\nEnrolando el servicio "%s" contra el vault…', ns)
|
|
@@ -136,7 +139,7 @@ async function cmdEnroll () {
|
|
|
136
139
|
function cmdStatus () {
|
|
137
140
|
const found = listEnrolled()
|
|
138
141
|
if (!found.length) {
|
|
139
|
-
console.log('Ningún servicio enrolado en %s\n Enrola uno: dotrino-env enroll --
|
|
142
|
+
console.log('Ningún servicio enrolado en %s\n Enrola uno: dotrino-env enroll --vault <bóveda> --ns <ns>', serviceRoot())
|
|
140
143
|
return
|
|
141
144
|
}
|
|
142
145
|
for (const e of found) {
|
|
@@ -144,11 +147,11 @@ function cmdStatus () {
|
|
|
144
147
|
// fingir que la migración no hace falta; callarlo dejaría un servicio «desaparecido».
|
|
145
148
|
if (e.legacy) {
|
|
146
149
|
const viejo = path.join(serviceRoot(), e.ns)
|
|
147
|
-
console.log('%s ⚠ del formato anterior, sin
|
|
148
|
-
console.log(' muévelo: mkdir -p %s/<
|
|
150
|
+
console.log('%s ⚠ del formato anterior, sin la bóveda — no se usa', e.ns)
|
|
151
|
+
console.log(' muévelo: mkdir -p %s/<bóveda> && mv %s %s/<bóveda>/', serviceRoot(), viejo, serviceRoot())
|
|
149
152
|
continue
|
|
150
153
|
}
|
|
151
|
-
const dir = serviceDir(e.
|
|
154
|
+
const dir = serviceDir(e.vault, e.ns)
|
|
152
155
|
const id = readServiceIdentity(dir)
|
|
153
156
|
const c = id?.cert
|
|
154
157
|
// El papel se describe por el ACTA a la que se ató. Uno del modelo viejo se dice tal
|
|
@@ -158,7 +161,7 @@ function cmdStatus () {
|
|
|
158
161
|
? `acta #${c.seq}`
|
|
159
162
|
: (typeof c?.exp === 'number' ? `modelo viejo · vence ${new Date(c.exp).toISOString().slice(0, 10)}` : '?')
|
|
160
163
|
console.log('%s/%s\n dir: %s\n vault: %s…\n scope: %s\n cert: %s',
|
|
161
|
-
e.
|
|
164
|
+
e.vault, e.ns, dir, String(id.iss).slice(0, 24), (c?.scope || []).join(', '), papel)
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
167
|
|
|
@@ -167,7 +170,7 @@ async function cmdCheck () {
|
|
|
167
170
|
// `fetchSecrets` y no `loadEnv`: listar NO debe tener efectos secundarios. Con
|
|
168
171
|
// `loadEnv` esto inyectaría el bundle en el entorno del propio `check`, que es
|
|
169
172
|
// justo lo que un comando de diagnóstico no tiene por qué hacer.
|
|
170
|
-
const secrets = await fetchSecrets({ dir: serviceDir(
|
|
173
|
+
const secrets = await fetchSecrets({ dir: serviceDir(etiquetaBoveda(), ns), ns })
|
|
171
174
|
const keys = Object.keys(secrets)
|
|
172
175
|
console.log('ns "%s": %d secreto(s)%s', ns, keys.length, keys.length ? ':' : '')
|
|
173
176
|
for (const k of keys) console.log(' ' + k) // NUNCA los valores
|
|
@@ -181,9 +184,15 @@ async function cmdCheck () {
|
|
|
181
184
|
async function cmdRun () {
|
|
182
185
|
const sep = argv.indexOf('--')
|
|
183
186
|
const cmd = sep >= 0 ? argv.slice(sep + 1) : []
|
|
184
|
-
if (!cmd.length) { console.error('uso: dotrino-env run --
|
|
187
|
+
if (!cmd.length) { console.error('uso: dotrino-env run --vault <bóveda> [--ns <ns>] -- <cmd> [args…]'); process.exit(2) }
|
|
185
188
|
const onPending = () => console.error('[dotrino-env] waiting for approval on your phone…')
|
|
186
|
-
|
|
189
|
+
// POR QUÉ SE REINTENTA, SIEMPRE. Sin esto la espera es muda: la bóveda contestaba, la
|
|
190
|
+
// respuesta se caía por un motivo concreto —un aparato sin sobre en su cajón, por
|
|
191
|
+
// ejemplo— y por fuera solo se veía «waiting for approval» una y otra vez, aunque el
|
|
192
|
+
// dueño hubiera aprobado cada una. Un reintento que no dice su causa es un fallback: le
|
|
193
|
+
// pone cara de espera a un error. (Ver CLAUDE.md, «nada de repliegues».)
|
|
194
|
+
const onRetry = (e, ms) => console.error('[dotrino-env] failed: %s — retrying in %ds', e.message, Math.round(ms / 1000))
|
|
195
|
+
const { injected, overridden } = await loadEnv({ ns: flag('ns'), vault: etiquetaBoveda(), onPending, onRetry })
|
|
187
196
|
console.error('[dotrino-env] %d valor(es) en el entorno de %s%s', injected.length, cmd[0],
|
|
188
197
|
overridden.length ? ` (pisados: ${overridden.join(', ')})` : '')
|
|
189
198
|
const child = spawn(cmd[0], cmd.slice(1), { stdio: 'inherit', env: process.env })
|
|
@@ -205,7 +214,7 @@ async function cmdSshAgent () {
|
|
|
205
214
|
const { serviceDir, resolveNs } = await import('../src/env.js')
|
|
206
215
|
const { waitForSecrets } = await import('../src/service.js')
|
|
207
216
|
const ns = resolveNs(flag('ns') || undefined)
|
|
208
|
-
const dir = flag('dir') || serviceDir(
|
|
217
|
+
const dir = flag('dir') || serviceDir(etiquetaBoveda(), ns)
|
|
209
218
|
const socketPath = flag('socket') || defaultSocketPath(dir)
|
|
210
219
|
console.error('[dotrino-env] asking the vault for the keys of «%s»…', ns)
|
|
211
220
|
const secrets = await waitForSecrets({ dir, ns, onPending: () => console.error('[dotrino-env] waiting for approval on your phone…') })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.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",
|
package/src/env.js
CHANGED
|
@@ -30,11 +30,15 @@ export function serviceRoot () {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
33
|
+
* DE QUÉ BÓVEDA VINO ESTO: `~/.dotrino/service/<bóveda>/<ns>`.
|
|
34
34
|
*
|
|
35
|
-
* Es una ETIQUETA TUYA, de esta máquina
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* Es una ETIQUETA TUYA, de esta máquina: nadie la manda por la red, no entra en el acta y
|
|
36
|
+
* no se compara con nada — puedes llamarla como quieras. Sirve solo para que dos bóvedas
|
|
37
|
+
* puedan tener un cajón que se llame igual.
|
|
38
|
+
*
|
|
39
|
+
* Se llama `--vault` y no `--profile` a propósito (dueño): `dotrino-vault --profile` ya
|
|
40
|
+
* significa OTRA cosa —el perfil de la bóveda— y los dos comandos se usan seguidos. Dos
|
|
41
|
+
* banderas con el mismo nombre y distinto significado se confunden el día que hay prisa.
|
|
38
42
|
*
|
|
39
43
|
* Antes la ruta era `<raíz>/<ns>` y el nombre del cajón era una CLAVE GLOBAL del usuario:
|
|
40
44
|
* si dos bóvedas usaban `aws`, el segundo enrolamiento pisaba al primero y lo dejaba
|
|
@@ -44,23 +48,23 @@ export function serviceRoot () {
|
|
|
44
48
|
*
|
|
45
49
|
* Mismo alfabeto que el `ns` porque es lo mismo: un segmento de ruta.
|
|
46
50
|
*/
|
|
47
|
-
export const
|
|
51
|
+
export const isValidVaultLabel = (p) => typeof p === 'string' && /^[a-z0-9-]{1,32}$/.test(p)
|
|
48
52
|
|
|
49
|
-
/** Directorio de la identidad del servicio `ns`
|
|
50
|
-
export function serviceDir (
|
|
53
|
+
/** Directorio de la identidad del servicio `ns` bajo la etiqueta de bóveda `vault`. */
|
|
54
|
+
export function serviceDir (vault, ns) {
|
|
51
55
|
if (process.env.DOTRINO_ENV_DIR) return process.env.DOTRINO_ENV_DIR
|
|
52
|
-
if (!
|
|
53
|
-
throw new Error('missing
|
|
56
|
+
if (!isValidVaultLabel(vault)) {
|
|
57
|
+
throw new Error('missing vault label: pass --vault <nombre> (use [a-z0-9-]{1,32}). It is a label of YOUR machine: it says which vault this drawer came from')
|
|
54
58
|
}
|
|
55
59
|
if (!isValidSecretsNs(ns)) throw new Error('invalid ns (use [a-z0-9-]{1,32}, e.g. "myapp")')
|
|
56
|
-
return path.join(serviceRoot(),
|
|
60
|
+
return path.join(serviceRoot(), vault, ns)
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
/**
|
|
60
|
-
export const
|
|
63
|
+
/** La etiqueta de bóveda: `--vault`, o `DOTRINO_ENV_VAULT`. */
|
|
64
|
+
export const resolveVaultLabel = (v) => v || process.env.DOTRINO_ENV_VAULT || null
|
|
61
65
|
|
|
62
66
|
/**
|
|
63
|
-
* Lo enrolado en esta máquina: `[{
|
|
67
|
+
* Lo enrolado en esta máquina: `[{ vault, ns }]`. Recorre DOS niveles.
|
|
64
68
|
*
|
|
65
69
|
* Y avisa de lo que quedó en el nivel de antes (`<raíz>/<ns>`): no se lee ni se usa —
|
|
66
70
|
* repescarlo en silencio sería fingir que la migración no hace falta— pero callarlo
|
|
@@ -71,14 +75,14 @@ export function listEnrolled () {
|
|
|
71
75
|
let dirs = []
|
|
72
76
|
try { dirs = fs.readdirSync(serviceRoot(), { withFileTypes: true }) } catch (_) { return out }
|
|
73
77
|
for (const d of dirs) {
|
|
74
|
-
if (!d.isDirectory() || !
|
|
78
|
+
if (!d.isDirectory() || !isValidVaultLabel(d.name)) continue
|
|
75
79
|
// Un enrolamiento del formato viejo es un directorio con `service-identity.json`
|
|
76
80
|
// DENTRO; uno del nuevo tiene subdirectorios. Así se distinguen sin adivinar.
|
|
77
81
|
if (readServiceIdentity(path.join(serviceRoot(), d.name))) { out.push({ legacy: true, ns: d.name }); continue }
|
|
78
82
|
let hijos = []
|
|
79
83
|
try { hijos = fs.readdirSync(path.join(serviceRoot(), d.name)) } catch (_) { continue }
|
|
80
84
|
for (const ns of hijos) {
|
|
81
|
-
if (isValidSecretsNs(ns) && readServiceIdentity(path.join(serviceRoot(), d.name, ns))) out.push({
|
|
85
|
+
if (isValidSecretsNs(ns) && readServiceIdentity(path.join(serviceRoot(), d.name, ns))) out.push({ vault: d.name, ns })
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
return out
|
|
@@ -139,12 +143,12 @@ let lastApplied = null
|
|
|
139
143
|
* `overridden` = las que YA tenían otro valor en el entorno y el vault pisó. Es
|
|
140
144
|
* el dato que delata un `.env` rancio, así que se reporta en vez de callarse.
|
|
141
145
|
*/
|
|
142
|
-
export async function loadEnv ({ ns,
|
|
146
|
+
export async function loadEnv ({ ns, vault, dir, override, wait = true, required = [], onRetry, onPending } = {}) {
|
|
143
147
|
if (override === undefined) override = overrideByDefault()
|
|
144
148
|
ns = resolveNs(ns)
|
|
145
|
-
//
|
|
146
|
-
// que dos bóvedas puedan tener un cajón con el mismo nombre sin pisarse.
|
|
147
|
-
dir = dir || serviceDir(
|
|
149
|
+
// La ETIQUETA DE BÓVEDA es obligatoria salvo que se diga el directorio a pelo: es lo que
|
|
150
|
+
// hace que dos bóvedas puedan tener un cajón con el mismo nombre sin pisarse.
|
|
151
|
+
dir = dir || serviceDir(resolveVaultLabel(vault), ns)
|
|
148
152
|
const load = wait ? waitForSecrets : fetchSecrets
|
|
149
153
|
const secrets = await load({ dir, ns, onRetry, onPending })
|
|
150
154
|
|
|
@@ -242,9 +246,9 @@ export function applyEnv (secrets, override = overrideByDefault()) {
|
|
|
242
246
|
* también. Se elige 0 porque salir a propósito no es un fallo.
|
|
243
247
|
* @returns {Promise<{stop:()=>void}>}
|
|
244
248
|
*/
|
|
245
|
-
export async function watchEnv ({ ns,
|
|
249
|
+
export async function watchEnv ({ ns, vault, dir, applied = lastApplied ?? undefined, onUpdate, exitCode = 0, quiet = false, ...rest } = {}) {
|
|
246
250
|
ns = resolveNs(ns)
|
|
247
|
-
dir = dir || serviceDir(
|
|
251
|
+
dir = dir || serviceDir(resolveVaultLabel(vault), ns)
|
|
248
252
|
const say = (m) => { if (!quiet) console.error(m) }
|
|
249
253
|
|
|
250
254
|
const exitNow = (reason) => {
|