@dotrino/vault 0.83.0 → 0.85.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/package.json +2 -2
- package/src/enroll.js +8 -4
- package/src/index.js +4 -1
- package/src/proc.js +12 -0
- package/src/service.js +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.85.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",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"pairing"
|
|
60
60
|
],
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"@dotrino/identity": ">=0.
|
|
62
|
+
"@dotrino/identity": ">=0.115.0",
|
|
63
63
|
"@dotrino/proxy-client": ">=0.23.1"
|
|
64
64
|
},
|
|
65
65
|
"license": "MIT",
|
package/src/enroll.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
import { verifyDeviceSig, pubkeyId, commitCode } from '@dotrino/identity/capabilities'
|
|
33
33
|
import { verifyContinuity, canSeal, DEVICE_CAPS, VAULT_CN } from '@dotrino/identity/acta'
|
|
34
|
+
import { cleanMachine } from '@dotrino/identity/machine'
|
|
34
35
|
|
|
35
36
|
/** Un token de emparejamiento vale 5 min. */
|
|
36
37
|
export const PAIRING_TTL_MS = 5 * 60 * 1000
|
|
@@ -113,7 +114,7 @@ export async function deviceIdOf (pub) {
|
|
|
113
114
|
* @param {(pub:string, obj:object)=>void} opts.sendByPubkey dirigir por pubkey (cola offline 24 h).
|
|
114
115
|
* @param {(op:string, info?:object)=>void} [opts.audit]
|
|
115
116
|
* @param {(...a:any[])=>void} [opts.log]
|
|
116
|
-
* @param {(c:{deviceId:string, scope:any, label:string})=>void} [opts.onChallenge] un dispositivo espera aprobación.
|
|
117
|
+
* @param {(c:{deviceId:string, scope:any, label:string, machine?:any, mode?:string})=>void} [opts.onChallenge] un dispositivo espera aprobación.
|
|
117
118
|
* @param {()=>void} [opts.onPendingChange]
|
|
118
119
|
* @param {(sub:string)=>void} [opts.onDeviceRemoved] se quitó un aparato (fuera del acta y sin papeles):
|
|
119
120
|
* para que quien guarde algo indexado por esa llave lo suelte. Se avisa desde AQUÍ y no desde
|
|
@@ -210,7 +211,7 @@ export function createEnrollDesk ({
|
|
|
210
211
|
function listPending () {
|
|
211
212
|
return [...pending.values()]
|
|
212
213
|
.filter((p) => p.state === 'PENDING_CONFIRM')
|
|
213
|
-
.map((p) => ({ deviceId: p.deviceId, label: p.label || '', scope: p.scope }))
|
|
214
|
+
.map((p) => ({ deviceId: p.deviceId, label: p.label || '', scope: p.scope, machine: p.machine || null }))
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
function findPending (deviceId) {
|
|
@@ -314,12 +315,15 @@ export function createEnrollDesk ({
|
|
|
314
315
|
// cuál era cuál. Si en `pair` le diste un nombre, ese es el nombre; el del aparato
|
|
315
316
|
// sigue valiendo como propuesta cuando no dijiste nada.
|
|
316
317
|
if (d.label && !pend.label) pend.label = String(d.label).slice(0, 60)
|
|
318
|
+
// DÓNDE CORRE (`@dotrino/identity/machine`): entra al acta con el aparato, para que la
|
|
319
|
+
// lista lo distinga de los demás aunque todos se llamen igual.
|
|
320
|
+
pend.machine = cleanMachine(d.machine)
|
|
317
321
|
// Camino A: de qué cuenta estamos hablando. Se guarda para poder comprobar, cuando
|
|
318
322
|
// llegue el acta sellada, que es la que este dispositivo dijo que iba a entregar.
|
|
319
323
|
if (intent === 'adopt' && typeof d.profileId === 'string') pend.profileId = d.profileId
|
|
320
324
|
|
|
321
325
|
reply(from, { type: MSG_ENROLL_CHALLENGE, deviceId })
|
|
322
|
-
fire(onChallenge, { deviceId, scope: pend.scope, label: pend.label || '', mode: pend.mode || 'join' })
|
|
326
|
+
fire(onChallenge, { deviceId, scope: pend.scope, label: pend.label || '', machine: pend.machine || null, mode: pend.mode || 'join' })
|
|
323
327
|
fire(onPendingChange)
|
|
324
328
|
return { deviceId }
|
|
325
329
|
}
|
|
@@ -396,7 +400,7 @@ export function createEnrollDesk ({
|
|
|
396
400
|
const { cert } = await identity.signDelegation(pend.dpub, pend.scope, { ttlMs: pend.ttlMs, label: pend.label })
|
|
397
401
|
|
|
398
402
|
try {
|
|
399
|
-
await identity.admitMember({ pub: pend.dpub, encPub: pend.encPub || null, label: pend.label || '', cn, caps, cert, continuity: pend.continuity || null })
|
|
403
|
+
await identity.admitMember({ pub: pend.dpub, encPub: pend.encPub || null, label: pend.label || '', cn, caps, cert, continuity: pend.continuity || null, machine: pend.machine || null })
|
|
400
404
|
} catch (e) {
|
|
401
405
|
// El certificado ya está firmado y no se entrega: se revoca, para que no quede uno
|
|
402
406
|
// válido suelto. Si ni eso sale, se dice también.
|
package/src/index.js
CHANGED
|
@@ -217,7 +217,10 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
217
217
|
deviceId: x.sub ? await deviceIdOf(x.sub) : null, sub: x.sub || null,
|
|
218
218
|
label: x.label || '', scope: x.scope, seq: x.seq, nonce: x.nonce
|
|
219
219
|
})))
|
|
220
|
-
|
|
220
|
+
// La cadena de selladores va con la lista (como en el daemon): es lo que un aparato de
|
|
221
|
+
// Node necesita para firmar a nombre de la cuenta de forma que un extraño lo compruebe.
|
|
222
|
+
const sealerChain = await identity.sealerChain?.().catch(() => null) || null
|
|
223
|
+
send(from, { type: MSG.DEVICES_RESULT, devices, revoked: (revoked || []).map((r) => r.nonce || r), sealerChain })
|
|
221
224
|
// ¿el que consulta es una máquina revocada que reapareció? → re-emite el REVOKED firmado.
|
|
222
225
|
// Se busca en `revokedCerts`: desde que `issued` solo trae lo vigente, los retirados
|
|
223
226
|
// ya no están ahí y este aviso dejaba de dispararse (se caía en silencio).
|
package/src/proc.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import fs from 'node:fs'
|
|
31
31
|
import os from 'node:os'
|
|
32
32
|
import crypto from 'node:crypto'
|
|
33
|
+
import { cleanMachine } from '@dotrino/identity/machine'
|
|
33
34
|
|
|
34
35
|
/** Topes. Un `argv` puede ser enorme y esto viaja en cada pedido y se pinta en un teléfono. */
|
|
35
36
|
export const MAX_ARGV_BYTES = 4096
|
|
@@ -131,6 +132,17 @@ export function selfContext () {
|
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
/**
|
|
136
|
+
* DÓNDE CORRE ESTE PROCESO (`@dotrino/identity/machine`): para que la bóveda lo reconozca en
|
|
137
|
+
* su lista de aparatos. Va en lo que el aparato firma al emparejarse y en cada pedido de
|
|
138
|
+
* secretos, así que se refresca solo cuando cambia de máquina o de sistema.
|
|
139
|
+
*/
|
|
140
|
+
export function selfMachine () {
|
|
141
|
+
const tipo = os.type()
|
|
142
|
+
const sistema = tipo === 'Darwin' ? 'macOS' : tipo === 'Windows_NT' ? 'Windows' : tipo
|
|
143
|
+
return cleanMachine({ kind: 'node', os: `${sistema} ${os.release()}`, name: os.hostname() })
|
|
144
|
+
}
|
|
145
|
+
|
|
134
146
|
const safeCwd = () => { try { return process.cwd() } catch (_) { return '' } }
|
|
135
147
|
const userName = () => { try { return os.userInfo().username } catch (_) { return '' } }
|
|
136
148
|
|
package/src/service.js
CHANGED
|
@@ -32,7 +32,7 @@ import { makeEphemeralKey, openSealed } from './sealed.js'
|
|
|
32
32
|
import { parseInvite } from './invite.js'
|
|
33
33
|
import { atRestFor } from './atrest.js'
|
|
34
34
|
import { connectLocal, hasLocalVault } from './localdesk.js'
|
|
35
|
-
import { selfContext } from './proc.js'
|
|
35
|
+
import { selfContext, selfMachine } from './proc.js'
|
|
36
36
|
|
|
37
37
|
const IDENTITY_FILE = 'service-identity.json'
|
|
38
38
|
const FRESH_WINDOW_MS = 5 * 60 * 1000
|
|
@@ -378,7 +378,7 @@ export async function enrollWithVault ({ qr, label = 'agent', expectedScope = nu
|
|
|
378
378
|
// aprende solo cuando un humano lo tipea → aprobar exige TENER esta máquina.
|
|
379
379
|
const code = makePairingCode()
|
|
380
380
|
const commit = await commitCode({ code, dpub: device.publickey, sn: qr.sn })
|
|
381
|
-
const data = { op: 'enroll', intent: 'join', dpub: device.publickey, encPub: enc?.encPublickey || null, token: qr.token || qr.sn, sn: qr.sn, commit, label, ts: Date.now() }
|
|
381
|
+
const data = { op: 'enroll', intent: 'join', dpub: device.publickey, encPub: enc?.encPublickey || null, token: qr.token || qr.sn, sn: qr.sn, commit, label, machine: selfMachine(), ts: Date.now() }
|
|
382
382
|
const { signature } = await signWithDevice({ privateJwk: device.privateJwk, data })
|
|
383
383
|
|
|
384
384
|
const enrolled = new Promise((resolve, reject) => {
|
|
@@ -908,7 +908,7 @@ export async function fetchSecrets ({ dir, ns, proxyUrl, masterPubkey, device, c
|
|
|
908
908
|
const ctx = client.local ? selfContext() : null
|
|
909
909
|
// `publicOnly` VIAJA FIRMADO (va dentro de `data`), así que nadie lo puede quitar ni
|
|
910
910
|
// poner por el camino. Quien decide qué se manda es la bóveda; esto solo es la petición.
|
|
911
|
-
const data = { op: 'secrets', ns, ek: eph.ek, publickey: device.publickey, ts: Date.now(), v: MY_VERSION, ...(ctx ? { ctx } : {}), ...(publicOnly ? { publicOnly: true } : {}) }
|
|
911
|
+
const data = { op: 'secrets', ns, ek: eph.ek, publickey: device.publickey, ts: Date.now(), v: MY_VERSION, machine: selfMachine(), ...(ctx ? { ctx } : {}), ...(publicOnly ? { publicOnly: true } : {}) }
|
|
912
912
|
const { signature } = await signWithDevice({ privateJwk: device.privateJwk, data })
|
|
913
913
|
// A QUIÉN SE LE PREGUNTA: primero a la bóveda, y si no contesta, a los REPLICADORES
|
|
914
914
|
// que el acta nombraba la última vez que la vimos (`docs/replicas.md` §8: «el servicio
|