@dotrino/vault 0.69.0 → 0.71.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/bin/dotrino-env.js +39 -2
- package/package.json +3 -2
- package/src/passwordLogins.js +24 -15
package/bin/dotrino-env.js
CHANGED
|
@@ -24,8 +24,38 @@ import { loadEnv, serviceDir, serviceRoot, listEnrolled, resolveNs, resolveVault
|
|
|
24
24
|
// proxy) lo hizo con su propio `enroll-vault.js` en vez de con esta CLI.
|
|
25
25
|
import { parseInvite as sharedParseInvite } from '../src/invite.js'
|
|
26
26
|
import { selfContext, commandLine } from '../src/proc.js'
|
|
27
|
+
import { updateNotice } from '@dotrino/update/notice'
|
|
28
|
+
import { PKG_VERSION as VERSION } from '../src/version.js'
|
|
27
29
|
|
|
28
30
|
const argv = process.argv.slice(2)
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* ¿HAY UN dotrino-env MÁS NUEVO? (CONVENCIONES §15)
|
|
34
|
+
*
|
|
35
|
+
* La regla que manda en un comando: **avisar no puede hacer lenta la orden**. Por eso la
|
|
36
|
+
* consulta ARRANCA aquí, antes de hacer nada, y se recoge al terminar: mientras corre lo que
|
|
37
|
+
* el usuario pidió —un `run` dura lo que dure su programa— la pregunta ya está contestada, y
|
|
38
|
+
* lo normal es que no cueste ni un milisegundo. Una vez al día; el resto sale de caché.
|
|
39
|
+
*
|
|
40
|
+
* Se compara contra `@dotrino/env`, que es lo que la gente instala, con la versión del
|
|
41
|
+
* código que corre: son la misma por construcción (`packaging/release-env.mjs` las fija
|
|
42
|
+
* iguales al publicar).
|
|
43
|
+
*
|
|
44
|
+
* La línea va por STDERR: `dotrino-env run` le cede stdout a otro programa, y colarle ahí
|
|
45
|
+
* una cortesía es romperle la tubería a quien lo esté canalizando.
|
|
46
|
+
*/
|
|
47
|
+
const avisoVersion = updateNotice({
|
|
48
|
+
current: VERSION, source: 'npm', pkg: '@dotrino/env', product: 'dotrino-env'
|
|
49
|
+
}).catch(() => null)
|
|
50
|
+
|
|
51
|
+
/** Lo dice, si hay algo que decir, con un tope: nunca retiene la salida más de 1,5 s. */
|
|
52
|
+
let avisoDicho = false
|
|
53
|
+
async function decirAvisoVersion () {
|
|
54
|
+
if (avisoDicho) return
|
|
55
|
+
avisoDicho = true
|
|
56
|
+
const linea = await Promise.race([avisoVersion, new Promise((resolve) => setTimeout(() => resolve(null), 1500).unref())])
|
|
57
|
+
if (linea) process.stderr.write(`\n[dotrino-env] ${linea} · actualiza con: npm i -g @dotrino/env\n`)
|
|
58
|
+
}
|
|
29
59
|
const flag = (name) => { const i = argv.indexOf('--' + name); return i >= 0 ? argv[i + 1] : undefined }
|
|
30
60
|
|
|
31
61
|
/**
|
|
@@ -275,7 +305,8 @@ async function cmdRun () {
|
|
|
275
305
|
console.error('[dotrino-env] %d valor(es) en el entorno de %s%s', injected.length, cmd[0],
|
|
276
306
|
overridden.length ? ` (pisados: ${overridden.join(', ')})` : '')
|
|
277
307
|
const child = spawn(cmd[0], cmd.slice(1), { stdio: 'inherit', env: process.env })
|
|
278
|
-
|
|
308
|
+
// Al terminar el programa, no antes: mientras corría, la consulta ya se contestó.
|
|
309
|
+
child.on('exit', async (code, signal) => { await decirAvisoVersion(); process.exit(signal ? 1 : (code ?? 0)) })
|
|
279
310
|
}
|
|
280
311
|
|
|
281
312
|
/**
|
|
@@ -329,4 +360,10 @@ const run = async () => {
|
|
|
329
360
|
}
|
|
330
361
|
}
|
|
331
362
|
|
|
332
|
-
run()
|
|
363
|
+
run()
|
|
364
|
+
// `run` y `ssh-agent` NO avisan aquí, y hay que hacerlo cumplir, no solo decirlo: `cmdRun`
|
|
365
|
+
// termina en cuanto LANZA el programa, así que este `.then` lo diría mientras el programa
|
|
366
|
+
// aún corre, mezclado con su salida. `run` avisa cuando su hijo sale; `ssh-agent` no
|
|
367
|
+
// termina nunca. Aquí llegan las órdenes cortas.
|
|
368
|
+
.then(() => (['run', 'ssh-agent'].includes(argv[0]) ? null : decirAvisoVersion()))
|
|
369
|
+
.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.71.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",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"type-check": "tsc --noEmit"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@dotrino/opaque": "0.1.0"
|
|
76
|
+
"@dotrino/opaque": "0.1.0",
|
|
77
|
+
"@dotrino/update": "0.1.1"
|
|
77
78
|
}
|
|
78
79
|
}
|
package/src/passwordLogins.js
CHANGED
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { server as opaquePorDefecto, suiteId as suitePorDefecto } from '@dotrino/opaque'
|
|
28
28
|
import { pubkeyId } from '@dotrino/identity/capabilities'
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
29
|
+
import { DEVICE_CAPS, capScope } from '@dotrino/identity/acta'
|
|
30
|
+
import { deviceIdOf } from './enroll.js'
|
|
31
31
|
import { bytesToB64url, b64urlToBytes } from './b64.js'
|
|
32
32
|
|
|
33
33
|
/** Un usuario es la parte de antes de la `@` en `nombre@AB12-CD34-EF56`. */
|
|
@@ -445,24 +445,35 @@ export function createLoginDesk ({ load, save, now = () => Date.now(), opaque =
|
|
|
445
445
|
* @param {object} opts
|
|
446
446
|
* `identity` la identidad que firma (`signDelegation`, `admitMember`)
|
|
447
447
|
* `logins` el escritorio de `createLoginDesk`
|
|
448
|
-
* `
|
|
449
|
-
*
|
|
448
|
+
* `caps` los PERMISOS del acta que lleva el aparato, cualquiera de `DEVICE_CAPS`;
|
|
449
|
+
* por defecto firmar, leer y guardar
|
|
450
450
|
*/
|
|
451
451
|
export async function registerLogin ({
|
|
452
452
|
identity, logins, user, upload, pub, encPub = null, label = '', blob,
|
|
453
|
-
|
|
453
|
+
caps = ['sign', 'read', 'store'], scope, replace = false
|
|
454
454
|
} = {}) {
|
|
455
455
|
if (replace) {
|
|
456
456
|
await logins.registerFinish({ user, upload, blob, replace: true })
|
|
457
457
|
return { ok: true, user, replaced: true }
|
|
458
458
|
}
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
459
|
+
// CUALQUIER PERMISO, y todos por el mismo camino (dueño, 2026-09-21: «al crear debe
|
|
460
|
+
// poder asignársele cualquier permiso»; regla dura «simplificar los procesos»). Antes
|
|
461
|
+
// llegaban como scopes del certificado con `unattended` aparte, `passwords` se rechazaba
|
|
462
|
+
// con `passwords-not-yet` y `passkeys` —que no tiene scope— no se podía asignar.
|
|
463
|
+
//
|
|
464
|
+
// `scope` ya no se acepta, y se dice: ignorarlo daría al aparato los permisos por defecto
|
|
465
|
+
// en vez de los que se eligieron, y nadie lo notaría.
|
|
466
|
+
if (scope !== undefined) {
|
|
467
|
+
throw Object.assign(new Error('registerLogin takes `caps` (account record permissions), not `scope`'), { code: 'use-caps' })
|
|
465
468
|
}
|
|
469
|
+
const pedidos = [...new Set(Array.isArray(caps) ? caps : [])]
|
|
470
|
+
const raros = pedidos.filter((c) => !DEVICE_CAPS.includes(c))
|
|
471
|
+
if (!pedidos.length || raros.length) {
|
|
472
|
+
throw Object.assign(new Error(`not device permissions: ${raros.join(', ') || '(none)'} — expected any of ${DEVICE_CAPS.join(', ')}`), { code: 'bad-caps' })
|
|
473
|
+
}
|
|
474
|
+
// El certificado lleva el scope de los permisos que lo tienen; los demás (`unattended`,
|
|
475
|
+
// `passkeys`) viven solo en el acta, que es quien decide.
|
|
476
|
+
const scopes = pedidos.map((c) => capScope(c)).filter(Boolean)
|
|
466
477
|
if (typeof identity?.admitMember !== 'function') {
|
|
467
478
|
throw Object.assign(new Error('this vault cannot add devices to the account record: no login was created'), { code: 'admit-unavailable' })
|
|
468
479
|
}
|
|
@@ -470,11 +481,9 @@ export async function registerLogin ({
|
|
|
470
481
|
await logins.registerFinish({ user, upload, pub, encPub, deviceId, label, blob })
|
|
471
482
|
try {
|
|
472
483
|
const { cert } = await identity.signDelegation(pub, scopes, { label: label || `login:${user}` })
|
|
473
|
-
|
|
474
|
-
const caps = [...new Set([...scopeToCaps(scopes), ...(unattended ? ['unattended'] : [])])]
|
|
475
|
-
await identity.admitMember({ pub, encPub, label: label || `login:${user}`, cn, caps, cert })
|
|
484
|
+
await identity.admitMember({ pub, encPub, label: label || `login:${user}`, cn: null, caps: pedidos, cert })
|
|
476
485
|
logins.setCert({ user, cert, iss: identity.me?.publickey || null })
|
|
477
|
-
return { ok: true, user, deviceId, cert, caps }
|
|
486
|
+
return { ok: true, user, deviceId, cert, caps: pedidos }
|
|
478
487
|
} catch (e) {
|
|
479
488
|
// NADA DE MEDIAS ALTAS: si no entra en el acta, no queda un usuario que pueda entrar a
|
|
480
489
|
// una cuenta que no lo reconoce. Se deshace y se dice.
|