@dotrino/vault 0.92.0 → 0.93.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 -38
- package/package.json +1 -1
- package/src/admin.js +2 -2
- package/src/atrest.js +4 -4
- package/src/browser.js +10 -10
- package/src/core/approvals.js +3 -2
- package/src/core/libVersion.js +2 -2
- package/src/core/sealer.js +2 -2
- package/src/core/sealers.js +15 -15
- package/src/core/secretsStore.js +66 -66
- package/src/core/subacta.js +8 -8
- package/src/core/threadStore.js +2 -2
- package/src/core/transport.js +31 -31
- package/src/core/vault.js +247 -214
- package/src/enroll.js +1 -1
- package/src/env.js +10 -10
- package/src/index.js +7 -7
- package/src/kek.js +6 -6
- package/src/localdesk.js +10 -10
- package/src/lock.js +18 -18
- package/src/loginClient.js +23 -23
- package/src/passwordLogins.js +8 -8
- package/src/proc.js +5 -5
- package/src/procContext.js +2 -2
- package/src/service.js +67 -43
- package/src/sshKeys.js +3 -3
- package/src/version.js +4 -4
package/bin/dotrino-env.js
CHANGED
|
@@ -46,17 +46,17 @@ const argv = process.argv.slice(2)
|
|
|
46
46
|
* La línea va por STDERR: `dotrino-env run` le cede stdout a otro programa, y colarle ahí
|
|
47
47
|
* una cortesía es romperle la tubería a quien lo esté canalizando.
|
|
48
48
|
*/
|
|
49
|
-
const
|
|
49
|
+
const versionNotice = updateNotice({
|
|
50
50
|
current: VERSION, source: 'npm', pkg: '@dotrino/env', product: 'dotrino-env'
|
|
51
51
|
}).catch(() => null)
|
|
52
52
|
|
|
53
53
|
/** Lo dice, si hay algo que decir, con un tope: nunca retiene la salida más de 1,5 s. */
|
|
54
|
-
let
|
|
55
|
-
async function
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
54
|
+
let noticeShown = false
|
|
55
|
+
async function showVersionNotice () {
|
|
56
|
+
if (noticeShown) return
|
|
57
|
+
noticeShown = true
|
|
58
|
+
const line = await Promise.race([versionNotice, new Promise((resolve) => setTimeout(() => resolve(null), 1500).unref())])
|
|
59
|
+
if (line) process.stderr.write(`\n[dotrino-env] ${line} · actualiza con: npm i -g @dotrino/env\n`)
|
|
60
60
|
}
|
|
61
61
|
const flag = (name) => { const i = argv.indexOf('--' + name); return i >= 0 ? argv[i + 1] : undefined }
|
|
62
62
|
|
|
@@ -68,7 +68,7 @@ const flag = (name) => { const i = argv.indexOf('--' + name); return i >= 0 ? ar
|
|
|
68
68
|
* lo que suena en el bolsillo — y con dos ventanas abiertas eso es exactamente lo que hay que
|
|
69
69
|
* distinguir antes de decir que sí.
|
|
70
70
|
*/
|
|
71
|
-
function
|
|
71
|
+
function awaitingApproval () {
|
|
72
72
|
const ctx = selfContext()
|
|
73
73
|
console.error('[dotrino-env] the vault asks for approval — approve it on a device with «aprueba» (up to 5 min)')
|
|
74
74
|
console.error('[dotrino-env] %s', commandLine(ctx, 160))
|
|
@@ -154,10 +154,10 @@ async function readInvite () {
|
|
|
154
154
|
* creando el directorio y hay que decir de quién es. Para USARLO no: el `ns` se busca entre
|
|
155
155
|
* las bóvedas enroladas y solo hace falta elegir si está en más de una (dueño, 2026-09-01).
|
|
156
156
|
*/
|
|
157
|
-
function
|
|
157
|
+
function vaultLabel ({ exigida: demanded = false } = {}) {
|
|
158
158
|
const v = resolveVaultLabel(flag('vault'))
|
|
159
159
|
if (!v) {
|
|
160
|
-
if (!
|
|
160
|
+
if (!demanded) return null
|
|
161
161
|
console.error('falta --vault <nombre> (o DOTRINO_ENV_VAULT)')
|
|
162
162
|
console.error('')
|
|
163
163
|
console.error(' Dice DE QUÉ BÓVEDA es este cajón. Es una etiqueta tuya, de esta máquina:')
|
|
@@ -191,23 +191,23 @@ async function cmdEnroll () {
|
|
|
191
191
|
// La etiqueta es solo el nombre de una carpeta de esta máquina, así que el nombre de la
|
|
192
192
|
// cuenta se lleva a minúsculas y se le quitan los caracteres que no valen. Si de ahí no
|
|
193
193
|
// sale nada utilizable, se pide — no se inventa un nombre.
|
|
194
|
-
let
|
|
195
|
-
if (!
|
|
194
|
+
let vault = vaultLabel()
|
|
195
|
+
if (!vault) {
|
|
196
196
|
const { vaultHello } = await import('../src/service.js')
|
|
197
197
|
// Se REUSA el qr que vuelve del hello: la cita se quema al canjearla, así que
|
|
198
198
|
// preguntar el nombre y después enrolar con el qr original gastaba la invitación
|
|
199
199
|
// en la pregunta y dejaba al enrolamiento sin nada que canjear.
|
|
200
200
|
qr = await vaultHello(qr).catch(() => qr)
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
if (!
|
|
201
|
+
const name = qr.acct || ''
|
|
202
|
+
vault = String(name || '').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32)
|
|
203
|
+
if (!vault) {
|
|
204
204
|
console.error('esa bóveda no dice cómo se llama su cuenta: dime tú la etiqueta')
|
|
205
205
|
console.error(' ej.: dotrino-env enroll --ns %s --vault mi-boveda --code …', ns)
|
|
206
206
|
process.exit(2)
|
|
207
207
|
}
|
|
208
|
-
console.log(' bóveda: «%s» (el nombre de la cuenta; se guarda en ~/.dotrino/service/%s/%s)',
|
|
208
|
+
console.log(' bóveda: «%s» (el nombre de la cuenta; se guarda en ~/.dotrino/service/%s/%s)', vault, vault, ns)
|
|
209
209
|
}
|
|
210
|
-
const dir = flag('dir') || serviceDir(
|
|
210
|
+
const dir = flag('dir') || serviceDir(vault, ns)
|
|
211
211
|
|
|
212
212
|
console.log('\nEnrolando el servicio "%s" contra el vault…', ns)
|
|
213
213
|
const { cert } = await enrollService({
|
|
@@ -252,9 +252,9 @@ async function cmdStatus () {
|
|
|
252
252
|
// LO QUE QUEDÓ EN EL FORMATO VIEJO se dice y no se usa. Repescarlo en silencio sería
|
|
253
253
|
// fingir que la migración no hace falta; callarlo dejaría un servicio «desaparecido».
|
|
254
254
|
if (e.legacy) {
|
|
255
|
-
const
|
|
255
|
+
const old = path.join(serviceRoot(), e.ns)
|
|
256
256
|
console.log('%s ⚠ del formato anterior, sin la bóveda — no se usa', e.ns)
|
|
257
|
-
console.log(' muévelo: mkdir -p %s/<bóveda> && mv %s %s/<bóveda>/', serviceRoot(),
|
|
257
|
+
console.log(' muévelo: mkdir -p %s/<bóveda> && mv %s %s/<bóveda>/', serviceRoot(), old, serviceRoot())
|
|
258
258
|
continue
|
|
259
259
|
}
|
|
260
260
|
const dir = serviceDir(e.vault, e.ns)
|
|
@@ -263,13 +263,13 @@ async function cmdStatus () {
|
|
|
263
263
|
// El papel se describe por el ACTA a la que se ató. Uno del modelo viejo se dice tal
|
|
264
264
|
// cual, con su fecha, porque ESO sí importa: es lo único que todavía caduca, y sabrá
|
|
265
265
|
// el operador que le queda una migración por hacer.
|
|
266
|
-
const
|
|
266
|
+
const paper = typeof c?.seq === 'number'
|
|
267
267
|
? `acta #${c.seq}`
|
|
268
268
|
: (typeof c?.exp === 'number' ? `modelo viejo · vence ${new Date(c.exp).toISOString().slice(0, 10)}` : '?')
|
|
269
|
-
const
|
|
270
|
-
const
|
|
269
|
+
const device = id?.device?.publickey ? await shortId(id.device.publickey) : '?'
|
|
270
|
+
const vault = id?.iss ? await shortId(id.iss) : '?'
|
|
271
271
|
console.log('%s/%s\n id: %s\n dir: %s\n vault: %s\n scope: %s\n cert: %s',
|
|
272
|
-
e.vault, e.ns,
|
|
272
|
+
e.vault, e.ns, device, dir, vault, (c?.scope || []).join(', '), paper)
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
|
|
@@ -328,10 +328,10 @@ async function cmdStore (fromFile) {
|
|
|
328
328
|
process.exit(1)
|
|
329
329
|
}
|
|
330
330
|
const allPublic = argv.includes('--all-public')
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
if (
|
|
334
|
-
const vars = parsed.items.map((it) => ({ key: it.key, value: it.value, public: allPublic ||
|
|
331
|
+
const publicOnes = new Set(String(flag('public') || '').split(',').map((k) => k.trim()).filter(Boolean))
|
|
332
|
+
const unknown = [...publicOnes].filter((k) => !parsed.items.some((it) => it.key === k))
|
|
333
|
+
if (unknown.length) { console.error('--public nombra variables que no se van a subir: %s', unknown.join(', ')); process.exit(2) }
|
|
334
|
+
const vars = parsed.items.map((it) => ({ key: it.key, value: it.value, public: allPublic || publicOnes.has(it.key) }))
|
|
335
335
|
|
|
336
336
|
// DE QUIÉN: la identidad de dotrino-env o, con --link, el enlace de un agente (terminal,
|
|
337
337
|
// ia, content…), que es un aparato con cajón igual que cualquier servicio.
|
|
@@ -345,17 +345,17 @@ async function cmdStore (fromFile) {
|
|
|
345
345
|
who = { ns, proxyUrl: link.proxy, masterPubkey: link.iss, device: link.device, cert: link.cert, dondeDice: `${linkDir} (cajón ${ns})` }
|
|
346
346
|
} else {
|
|
347
347
|
const ns = resolveNs(flag('ns'))
|
|
348
|
-
const dir = serviceDir(
|
|
348
|
+
const dir = serviceDir(vaultLabel(), ns)
|
|
349
349
|
who = { dir, ns, dondeDice: `${path.basename(path.dirname(dir))}/${ns}` }
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
const pub = vars.filter((v) => v.public).map((v) => v.key)
|
|
353
353
|
console.log('Subiendo %d variable(s) a %s: %s', vars.length, who.dondeDice, vars.map((v) => v.key).join(', '))
|
|
354
354
|
if (pub.length) console.log(' públicas: %s', pub.join(', '))
|
|
355
|
-
const { dondeDice, ...ident } = who
|
|
355
|
+
const { dondeDice: whereItSays, ...ident } = who
|
|
356
356
|
const r = await storeVars({ ...ident, vars }).catch((e) => {
|
|
357
357
|
if (e.code === 'exists') {
|
|
358
|
-
console.error('\n%s\nSin nadie que pueda aprobar, desde aquí solo se añaden variables nuevas. Para cambiar una que ya existe, hazlo en la bóveda (dotrino-vault secret set) o
|
|
358
|
+
console.error('\n%s\nSin nadie que pueda aprobar, desde aquí solo se añaden variables nuevas. Para cambiar una que ya existe, hazlo en la bóveda (dotrino-vault secret set) o asígnale a alguien el permiso de aprobar.', e.message)
|
|
359
359
|
} else console.error('\nno se pudo subir: %s', e.message)
|
|
360
360
|
process.exit(1)
|
|
361
361
|
})
|
|
@@ -380,8 +380,8 @@ async function cmdCheck () {
|
|
|
380
380
|
// bóveda contestó, que lo que falta es un permiso, ni cuánto va a esperar. `run` sí lo
|
|
381
381
|
// decía; `check`, que es el comando de DIAGNÓSTICO, era el único que no. Le pasó al dueño
|
|
382
382
|
// el 2026-09-05 con un cajón cuyo servicio no tenía a nadie que pudiera aprobarle.
|
|
383
|
-
const onPending = () =>
|
|
384
|
-
const secrets = await fetchSecrets({ dir: serviceDir(
|
|
383
|
+
const onPending = () => awaitingApproval()
|
|
384
|
+
const secrets = await fetchSecrets({ dir: serviceDir(vaultLabel(), ns), ns, publicOnly, onPending })
|
|
385
385
|
const keys = Object.keys(secrets)
|
|
386
386
|
console.log('ns "%s": %d %s%s', ns, keys.length,
|
|
387
387
|
publicOnly ? 'variable(s) pública(s)' : 'secreto(s)', keys.length ? ':' : '')
|
|
@@ -397,7 +397,7 @@ async function cmdRun () {
|
|
|
397
397
|
const sep = argv.indexOf('--')
|
|
398
398
|
const cmd = sep >= 0 ? argv.slice(sep + 1) : []
|
|
399
399
|
if (!cmd.length) { console.error('uso: dotrino-env run [--vault <bóveda>] [--ns <ns>] [--public] -- <cmd> [args…]'); process.exit(2) }
|
|
400
|
-
const onPending = () =>
|
|
400
|
+
const onPending = () => awaitingApproval()
|
|
401
401
|
// POR QUÉ SE REINTENTA, SIEMPRE. Sin esto la espera es muda: la bóveda contestaba, la
|
|
402
402
|
// respuesta se caía por un motivo concreto —un aparato sin sobre en su cajón, por
|
|
403
403
|
// ejemplo— y por fuera solo se veía «waiting for approval» una y otra vez, aunque el
|
|
@@ -412,12 +412,12 @@ async function cmdRun () {
|
|
|
412
412
|
// ya la ve quien administra. Es lo que quiere un arranque que solo necesita configuración
|
|
413
413
|
// —una URL, un puerto— y no tiene por qué despertar al dueño.
|
|
414
414
|
const publicOnly = argv.includes('--public')
|
|
415
|
-
const { injected, overridden } = await loadEnv({ ns: flag('ns'), vault:
|
|
415
|
+
const { injected, overridden } = await loadEnv({ ns: flag('ns'), vault: vaultLabel(), onPending, onRetry, publicOnly })
|
|
416
416
|
console.error('[dotrino-env] %d valor(es) en el entorno de %s%s', injected.length, cmd[0],
|
|
417
417
|
overridden.length ? ` (pisados: ${overridden.join(', ')})` : '')
|
|
418
418
|
const child = spawn(cmd[0], cmd.slice(1), { stdio: 'inherit', env: process.env })
|
|
419
419
|
// Al terminar el programa, no antes: mientras corría, la consulta ya se contestó.
|
|
420
|
-
child.on('exit', async (code, signal) => { await
|
|
420
|
+
child.on('exit', async (code, signal) => { await showVersionNotice(); process.exit(signal ? 1 : (code ?? 0)) })
|
|
421
421
|
}
|
|
422
422
|
|
|
423
423
|
/**
|
|
@@ -435,10 +435,10 @@ async function cmdSshAgent () {
|
|
|
435
435
|
const { serviceDir, resolveNs } = await import('../src/env.js')
|
|
436
436
|
const { waitForSecrets } = await import('../src/service.js')
|
|
437
437
|
const ns = resolveNs(flag('ns') || undefined)
|
|
438
|
-
const dir = flag('dir') || serviceDir(
|
|
438
|
+
const dir = flag('dir') || serviceDir(vaultLabel(), ns)
|
|
439
439
|
const socketPath = flag('socket') || defaultSocketPath(dir)
|
|
440
440
|
console.error('[dotrino-env] asking the vault for the keys of «%s»…', ns)
|
|
441
|
-
const secrets = await waitForSecrets({ dir, ns, onPending: () =>
|
|
441
|
+
const secrets = await waitForSecrets({ dir, ns, onPending: () => awaitingApproval() })
|
|
442
442
|
const keys = []
|
|
443
443
|
for (const [name, value] of Object.entries(secrets)) {
|
|
444
444
|
if (!/^SSH_KEY_[A-Z0-9_]*$/.test(name)) continue
|
|
@@ -479,5 +479,5 @@ run()
|
|
|
479
479
|
// termina en cuanto LANZA el programa, así que este `.then` lo diría mientras el programa
|
|
480
480
|
// aún corre, mezclado con su salida. `run` avisa cuando su hijo sale; `ssh-agent` no
|
|
481
481
|
// termina nunca. Aquí llegan las órdenes cortas.
|
|
482
|
-
.then(() => (['run', 'ssh-agent'].includes(argv[0]) ? null :
|
|
482
|
+
.then(() => (['run', 'ssh-agent'].includes(argv[0]) ? null : showVersionNotice()))
|
|
483
483
|
.catch((e) => { console.error('\n[dotrino-env] ' + e.message); process.exit(1) })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.93.1",
|
|
4
4
|
"description": "La bóveda de Dotrino como librería: el MISMO núcleo que el binario corriendo en el navegador (`@dotrino/vault/browser`), el mostrador de emparejamiento y 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
|
@@ -157,7 +157,7 @@ export function createAdminDesk ({
|
|
|
157
157
|
now = () => Date.now()
|
|
158
158
|
}) {
|
|
159
159
|
const ops = new Set(ADMIN_OPS)
|
|
160
|
-
const
|
|
160
|
+
const whileLocked = new Set(ADMIN_OPS_WHILE_LOCKED)
|
|
161
161
|
|
|
162
162
|
// NONCE de un solo uso. `sign`/`get` son idempotentes y les basta la ventana de ±5
|
|
163
163
|
// min; `revoke` y `var.set` CAMBIAN ESTADO, así que reproducir uno dentro de esa
|
|
@@ -196,7 +196,7 @@ export function createAdminDesk ({
|
|
|
196
196
|
//
|
|
197
197
|
// El código va aparte del texto porque el texto se traduce y el código no: quien
|
|
198
198
|
// llama distingue «está cerrada, ábrela» de «no tienes permiso» sin leer la frase.
|
|
199
|
-
if (isLocked() && !
|
|
199
|
+
if (isLocked() && !whileLocked.has(data.op)) {
|
|
200
200
|
audit('rejected', { what: 'admin', op: data.op, by, reason: 'locked' })
|
|
201
201
|
return {
|
|
202
202
|
ok: false,
|
package/src/atrest.js
CHANGED
|
@@ -169,18 +169,18 @@ export function atRestFor (dir, { password = '' } = {}) {
|
|
|
169
169
|
/**
|
|
170
170
|
* Cifra AHORA un archivo que esté en claro, verificando antes de reemplazar: se escribe a un
|
|
171
171
|
* temporal, se vuelve a leer y descifrar, se compara, y solo entonces se renombra encima.
|
|
172
|
-
* Devuelve '
|
|
172
|
+
* Devuelve 'already-encrypted' | 'migrated' | 'no-file'.
|
|
173
173
|
*/
|
|
174
174
|
export function migrateFile (file, key) {
|
|
175
175
|
let text
|
|
176
|
-
try { text = fs.readFileSync(file, 'utf8') } catch (_) { return '
|
|
177
|
-
if (isEncrypted(text)) return '
|
|
176
|
+
try { text = fs.readFileSync(file, 'utf8') } catch (_) { return 'no-file' }
|
|
177
|
+
if (isEncrypted(text)) return 'already-encrypted'
|
|
178
178
|
const blob = encryptText(text, key)
|
|
179
179
|
if (decryptText(blob, key) !== text) throw new Error('at-rest: verification failed, the original is untouched')
|
|
180
180
|
const tmp = file + '.atrest.tmp'
|
|
181
181
|
fs.writeFileSync(tmp, blob, { mode: 0o600 })
|
|
182
182
|
fs.renameSync(tmp, file)
|
|
183
|
-
return '
|
|
183
|
+
return 'migrated'
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
/** Los archivos de este directorio que están cifrados por nosotros. */
|
package/src/browser.js
CHANGED
|
@@ -69,7 +69,7 @@ export async function startBrowserVault ({ core, proxyUrl = DEFAULT_PROXY, makeC
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
const
|
|
72
|
+
const notify = () => { try { onPendingChange() } catch (_) {} }
|
|
73
73
|
const vault = await startVault({
|
|
74
74
|
platform: {
|
|
75
75
|
disk,
|
|
@@ -87,7 +87,7 @@ export async function startBrowserVault ({ core, proxyUrl = DEFAULT_PROXY, makeC
|
|
|
87
87
|
// El navegador no tiene la contraseña del perfil con la que el binario cierra la copia
|
|
88
88
|
// de recuperación: se cierra con la llave de este almacén, y el núcleo lo dice.
|
|
89
89
|
hasPassword: () => false,
|
|
90
|
-
onEnrollChallenge:
|
|
90
|
+
onEnrollChallenge: notify,
|
|
91
91
|
onAdopted: (info) => { try { onAdopted(info) } catch (_) {} }
|
|
92
92
|
})
|
|
93
93
|
|
|
@@ -109,7 +109,7 @@ export async function startBrowserVault ({ core, proxyUrl = DEFAULT_PROXY, makeC
|
|
|
109
109
|
* acta: es verdad que este navegador administra la cuenta. `aprueba` NO, a propósito: con
|
|
110
110
|
* él los servicios pasarían a esperar el visto bueno de una pestaña que puede no estar.
|
|
111
111
|
*/
|
|
112
|
-
const
|
|
112
|
+
const ensureAdmin = async () => {
|
|
113
113
|
if (core.masterLocked) return
|
|
114
114
|
const acta = (await identity.profileActa().catch(() => null))?.acta || null
|
|
115
115
|
const yo = (acta?.members || []).find((m) => m.pub === master)
|
|
@@ -119,15 +119,15 @@ export async function startBrowserVault ({ core, proxyUrl = DEFAULT_PROXY, makeC
|
|
|
119
119
|
log('[vault] this browser administers the account: admin added to its own key')
|
|
120
120
|
}
|
|
121
121
|
const open = (onReply) => vault.localChannel(onReply)
|
|
122
|
-
const
|
|
122
|
+
const present = async () => {
|
|
123
123
|
try {
|
|
124
|
-
await
|
|
124
|
+
await ensureAdmin()
|
|
125
125
|
const cert = await selfCert()
|
|
126
126
|
core.setSelfVault?.(cert ? { cert, proxy: proxyUrl, open } : null)
|
|
127
127
|
} catch (e) { log('[vault] the console in this browser cannot administer this vault yet: ' + (e?.message || e)) }
|
|
128
128
|
}
|
|
129
|
-
await
|
|
130
|
-
const
|
|
129
|
+
await present()
|
|
130
|
+
const refresh = setInterval(present, SELF_CERT_EVERY_MS)
|
|
131
131
|
|
|
132
132
|
return {
|
|
133
133
|
...vault,
|
|
@@ -135,8 +135,8 @@ export async function startBrowserVault ({ core, proxyUrl = DEFAULT_PROXY, makeC
|
|
|
135
135
|
proxy: proxyUrl,
|
|
136
136
|
disk,
|
|
137
137
|
/** Aprueba TIPEANDO el código que muestra el aparato (el núcleo comprueba el compromiso). */
|
|
138
|
-
approve: async (_deviceId, code) => { try { return await vault.approveDevice(code) } finally {
|
|
139
|
-
reject: (deviceId) => { try { return vault.rejectDevice(deviceId) } finally {
|
|
138
|
+
approve: async (_deviceId, code) => { try { return await vault.approveDevice(code) } finally { notify() } },
|
|
139
|
+
reject: (deviceId) => { try { return vault.rejectDevice(deviceId) } finally { notify() } },
|
|
140
140
|
/**
|
|
141
141
|
* Las máquinas con papel de firma de esta cuenta (excluye los navegadores enrolados con
|
|
142
142
|
* etiqueta `cli`, que no atienden pedidos). Se queda el papel del acta más nueva.
|
|
@@ -155,7 +155,7 @@ export async function startBrowserVault ({ core, proxyUrl = DEFAULT_PROXY, makeC
|
|
|
155
155
|
revokeDevice: (sub) => vault.revokeDevice({ sub }),
|
|
156
156
|
revoke: (nonce) => vault.revokeDevice(nonce),
|
|
157
157
|
async close () {
|
|
158
|
-
clearInterval(
|
|
158
|
+
clearInterval(refresh)
|
|
159
159
|
try { core.setSelfVault?.(null) } catch (_) {}
|
|
160
160
|
vault.close()
|
|
161
161
|
try { await disk.flush() } catch (e) { log('[vault] some changes could not be saved: ' + (e?.message || e)) }
|
package/src/core/approvals.js
CHANGED
|
@@ -84,7 +84,7 @@ export function createApprovals ({ now = Date.now, pendingTtlMs = PENDING_TTL_MS
|
|
|
84
84
|
* apruebe— y tiene que volver por el mismo sitio: desde que la bóveda atiende también
|
|
85
85
|
* por un socket local, mandarla siempre por el proxio la dejaba en el vacío.
|
|
86
86
|
*/
|
|
87
|
-
request ({ ns, device, deviceId, label = '', ek, ctx = null, from = null, ttlMs = null, kind = 'read' }) {
|
|
87
|
+
request ({ ns, device, deviceId, label = '', ek, ctx = null, from = null, ttlMs = null, kind = 'read', re = null }) {
|
|
88
88
|
// Pedir las claves otra vez reemplaza al pedido anterior: es la misma pregunta. Una
|
|
89
89
|
// ESCRITURA no: cada una lleva su propio valor, y reemplazarla tiraría la anterior
|
|
90
90
|
// sin que nadie dijera que no.
|
|
@@ -92,7 +92,8 @@ export function createApprovals ({ now = Date.now, pendingTtlMs = PENDING_TTL_MS
|
|
|
92
92
|
const ts = now()
|
|
93
93
|
// `ttlMs` lo pone quien pide: una actualización aguanta un día y un comando cinco
|
|
94
94
|
// minutos, y la mesa no tiene por qué saber cuál es cuál (ver `PENDING_TTL_MS`).
|
|
95
|
-
|
|
95
|
+
// `re`: la huella de la petición, para firmarle al que pidió el «denegado» (ver `signError`).
|
|
96
|
+
const p = { id: rnd(), ns, kind, device, deviceId, label, ek, ctx, ts, exp: ts + (ttlMs || pendingTtlMs), from, re }
|
|
96
97
|
pending.set(p.id, p)
|
|
97
98
|
return publicOf(p)
|
|
98
99
|
},
|
package/src/core/libVersion.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LA VERSIÓN DE `@dotrino/vault`, escrita aquí porque el navegador no puede leer el
|
|
3
3
|
* `package.json`. Con esta se anuncia la bóveda de la pestaña y de la extensión
|
|
4
|
-
* (`@dotrino/compat`). Una prueba (`test/version
|
|
4
|
+
* (`@dotrino/compat`). Una prueba (`test/browser-version.test.mjs`) para el release
|
|
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.
|
|
8
|
+
export const LIB_VERSION = '0.93.1'
|
package/src/core/sealer.js
CHANGED
|
@@ -163,8 +163,8 @@ export function makeSealer () {
|
|
|
163
163
|
* síntoma sería un servicio arrancando sin configuración y sin decir por qué.
|
|
164
164
|
*/
|
|
165
165
|
async wrapFor (cek, members) {
|
|
166
|
-
const { generation, sinLlave } = await makeGeneration({ members, cek })
|
|
167
|
-
return { wraps: generation.wraps, sinLlave }
|
|
166
|
+
const { generation, sinLlave: noKey } = await makeGeneration({ members, cek })
|
|
167
|
+
return { wraps: generation.wraps, sinLlave: noKey }
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
}
|
package/src/core/sealers.js
CHANGED
|
@@ -59,7 +59,7 @@ export function startSealersPublisher ({ identity, client, log = console.log, re
|
|
|
59
59
|
if (!registryId) return () => {}
|
|
60
60
|
|
|
61
61
|
/** Una respuesta a `sealers.whois`, o null si no contesta a tiempo. */
|
|
62
|
-
function
|
|
62
|
+
function askWhoItIs (token, timeoutMs = 8000) {
|
|
63
63
|
return new Promise((resolve) => {
|
|
64
64
|
const fin = (v) => { try { off() } catch (_) {} ; clearTimeout(t); resolve(v) }
|
|
65
65
|
const off = client.on('message', (_f, p) => { if (p?.op === WHOIS + '.result') fin(p) })
|
|
@@ -82,11 +82,11 @@ export function startSealersPublisher ({ identity, client, log = console.log, re
|
|
|
82
82
|
* filtraría nada —el eslabón es público— pero se los tragaría EN SILENCIO, que es peor
|
|
83
83
|
* que un error.
|
|
84
84
|
*/
|
|
85
|
-
async function
|
|
85
|
+
async function witness () {
|
|
86
86
|
let tokens
|
|
87
87
|
try { tokens = await client.list(canal) } catch (_) { return null }
|
|
88
88
|
for (const token of tokens || []) {
|
|
89
|
-
const r = await
|
|
89
|
+
const r = await askWhoItIs(token)
|
|
90
90
|
const cert = r?.cert
|
|
91
91
|
if (!cert?.sub || cert.iss !== registryId) continue
|
|
92
92
|
// AQUÍ SOMOS EXTRAÑOS: no tenemos el acta del registro, así que no podemos preguntarle
|
|
@@ -107,11 +107,11 @@ export function startSealersPublisher ({ identity, client, log = console.log, re
|
|
|
107
107
|
return null
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
let
|
|
110
|
+
let latest = null
|
|
111
111
|
|
|
112
|
-
let
|
|
112
|
+
let warnedOtherWitness = false
|
|
113
113
|
|
|
114
|
-
async function
|
|
114
|
+
async function publish (reason) {
|
|
115
115
|
let actas
|
|
116
116
|
try { actas = await identity.sealerChain?.() } catch (_) { return }
|
|
117
117
|
if (!Array.isArray(actas)) return
|
|
@@ -122,8 +122,8 @@ export function startSealersPublisher ({ identity, client, log = console.log, re
|
|
|
122
122
|
// exactamente lo que ese campo existe para impedir. Se dice una vez y se calla.
|
|
123
123
|
const genesis = actas[0]
|
|
124
124
|
if (genesis?.chainUrl && registryId === DOTRINO_REGISTRY_ID) {
|
|
125
|
-
if (!
|
|
126
|
-
|
|
125
|
+
if (!warnedOtherWitness) {
|
|
126
|
+
warnedOtherWitness = true
|
|
127
127
|
log(`[sealers] this account names its own registry (${genesis.chainUrl}): not depositing into Dotrino's`)
|
|
128
128
|
}
|
|
129
129
|
return
|
|
@@ -136,14 +136,14 @@ export function startSealersPublisher ({ identity, client, log = console.log, re
|
|
|
136
136
|
// aparece en el registro hasta que selle un acta nueva.
|
|
137
137
|
if (chain.length < 2) return
|
|
138
138
|
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
139
|
+
const head = chain[chain.length - 1]?.seq
|
|
140
|
+
if (head === latest) return
|
|
141
141
|
try {
|
|
142
|
-
const a = await
|
|
142
|
+
const a = await witness()
|
|
143
143
|
if (!a) { log('[sealers] no witness announced in ' + canal + ': the chain stays for the next time'); return }
|
|
144
144
|
await client.sendByPubkey(a, { op: OP, chain })
|
|
145
|
-
|
|
146
|
-
log(`[sealers] chain up to #${
|
|
145
|
+
latest = head
|
|
146
|
+
log(`[sealers] chain up to #${head} deposited (${reason})`)
|
|
147
147
|
} catch (e) {
|
|
148
148
|
// Que no se pueda depositar no rompe nada de lo que la bóveda hace: el registro es
|
|
149
149
|
// una comodidad para terceros, no una dependencia. Se reintenta en la próxima acta.
|
|
@@ -151,8 +151,8 @@ export function startSealersPublisher ({ identity, client, log = console.log, re
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
const off = identity.onVault?.((e) => { if (e?.phase === 'acta')
|
|
154
|
+
publish('startup').catch(() => {})
|
|
155
|
+
const off = identity.onVault?.((e) => { if (e?.phase === 'acta') publish(`record #${e.seq}`).catch(() => {}) })
|
|
156
156
|
return () => { try { off?.() } catch (_) {} }
|
|
157
157
|
}
|
|
158
158
|
|