@dotrino/vaultd 0.154.1 → 0.155.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/enroll.js +8 -4
- package/lib/src/proc.js +12 -0
- package/lib/src/service.js +3 -3
- package/package.json +2 -2
- package/src/ctl.js +3 -0
- package/src/daemon.js +2 -2
- package/src/machines.js +69 -0
- package/src/tui/app.js +13 -2
- package/src/vault.js +17 -2
package/lib/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/lib/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/lib/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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.155.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": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@dotrino/compat": "0.4.0",
|
|
25
|
-
"@dotrino/identity": "^0.
|
|
25
|
+
"@dotrino/identity": "^0.115.0",
|
|
26
26
|
"@dotrino/opaque": "0.1.0",
|
|
27
27
|
"@dotrino/passmanager": "0.17.0",
|
|
28
28
|
"@dotrino/proxy-client": "^0.32.2",
|
package/src/ctl.js
CHANGED
|
@@ -40,6 +40,7 @@ import { VERSION } from './version.js'
|
|
|
40
40
|
import * as hw from './hwkeys.js'
|
|
41
41
|
import { isNewer } from './update.js'
|
|
42
42
|
import { COMM_CN } from './commKey.js'
|
|
43
|
+
import { machineLine } from '@dotrino/identity/machine'
|
|
43
44
|
|
|
44
45
|
const dir = dataDir()
|
|
45
46
|
const stateFile = path.join(dir, 'state.json')
|
|
@@ -673,6 +674,8 @@ async function cmdMembers () {
|
|
|
673
674
|
if (m.renounced?.length) {
|
|
674
675
|
console.log(' %s↓ renunció a %s (pendiente de sellar: abre la bóveda)%s', R, m.renounced.join(', '), Z)
|
|
675
676
|
}
|
|
677
|
+
// DÓNDE CORRE (`@dotrino/identity/machine`): con qué se reconoce cuál es cuál.
|
|
678
|
+
if (machineLine(m.machine)) console.log(' %s%s%s', D, machineLine(m.machine), Z)
|
|
676
679
|
if (m.running) {
|
|
677
680
|
const v = `${m.running.product} ${m.running.version}`
|
|
678
681
|
if (m.running.compatible) console.log(' %scorre %s%s', D, v, Z)
|
package/src/daemon.js
CHANGED
|
@@ -87,8 +87,8 @@ export async function runDaemon () {
|
|
|
87
87
|
const pendingEnrollFile = path.join(dir, 'pending-enroll.json')
|
|
88
88
|
// Cuando un dispositivo pide enrolarse, exponemos su deviceId (y a QUÉ perfil
|
|
89
89
|
// quiere entrar) para que el dueño lo compare con el del dispositivo y apruebe.
|
|
90
|
-
const onEnrollChallenge = ({ deviceId, scope, profile, profileName }) => {
|
|
91
|
-
ipcWrite(pendingEnrollFile, { v: 2, at: Date.now(), deviceId, scope, profile, profileName })
|
|
90
|
+
const onEnrollChallenge = ({ deviceId, scope, profile, profileName, machine }) => {
|
|
91
|
+
ipcWrite(pendingEnrollFile, { v: 2, at: Date.now(), deviceId, scope, profile, profileName, machine: machine || null })
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
const mgr = await startVaultManager({ root: dir, proxyUrl, onEnrollChallenge })
|
package/src/machines.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DÓNDE CORRE CADA APARATO, según lo último que dijo (`@dotrino/identity/machine`).
|
|
3
|
+
*
|
|
4
|
+
* El acta guarda lo que el aparato dijo AL EMPAREJARSE, y eso no cambia sin una firma de la
|
|
5
|
+
* maestra. Aquí va lo que dice en cada pedido firmado: así un aparato que entró antes de que
|
|
6
|
+
* existiera el campo aparece con su máquina en cuanto se actualiza y habla, y uno que cambió
|
|
7
|
+
* de sistema sale al día. La lista enseña esto primero y, si no hay, lo del acta.
|
|
8
|
+
*
|
|
9
|
+
* NO DECIDE NADA: se enseña. Lo dice el propio aparato, dentro de lo que firma; nadie más lo
|
|
10
|
+
* puede escribir por él, pero tampoco prueba nada.
|
|
11
|
+
*
|
|
12
|
+
* Cifrado en reposo como lo demás. Un archivo que existe y no se puede leer se lanza.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import { cleanMachine, sameMachine } from '@dotrino/identity/machine'
|
|
17
|
+
|
|
18
|
+
export const MACHINES_FILE = 'machines.json'
|
|
19
|
+
|
|
20
|
+
const fail = (code, message) => Object.assign(new Error(message), { code })
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {{ dir: string, atRest: { encrypt: (s: string) => string, decrypt: (s: string) => string }, now?: () => number }} opts
|
|
24
|
+
*/
|
|
25
|
+
export function openMachines ({ dir, atRest, now = Date.now }) {
|
|
26
|
+
const file = path.join(dir, MACHINES_FILE)
|
|
27
|
+
/** pub → { kind, os, name, at } */
|
|
28
|
+
const items = load()
|
|
29
|
+
|
|
30
|
+
function load () {
|
|
31
|
+
let raw
|
|
32
|
+
try { raw = fs.readFileSync(file, 'utf8') } catch (e) {
|
|
33
|
+
if (e.code === 'ENOENT') return new Map()
|
|
34
|
+
throw fail('machines-unreadable', `cannot read ${MACHINES_FILE}: ${e.message}`)
|
|
35
|
+
}
|
|
36
|
+
let d
|
|
37
|
+
try { d = JSON.parse(atRest.decrypt(raw)) } catch (e) { throw fail('machines-unreadable', `${MACHINES_FILE} cannot be opened: ${e.message}`) }
|
|
38
|
+
if (d?.v !== 1 || !d.items || typeof d.items !== 'object') throw fail('machines-unreadable', `${MACHINES_FILE} has an unknown shape`)
|
|
39
|
+
return new Map(Object.entries(d.items))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function save () {
|
|
43
|
+
const tmp = file + '.tmp'
|
|
44
|
+
fs.writeFileSync(tmp, atRest.encrypt(JSON.stringify({ v: 1, items: Object.fromEntries(items) })), { mode: 0o600 })
|
|
45
|
+
fs.renameSync(tmp, file)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
/**
|
|
50
|
+
* Lo que acaba de decir un aparato. Solo se escribe si cambió: casi todos los pedidos
|
|
51
|
+
* repiten lo de siempre, y reescribir el archivo en cada uno sería disco para nada.
|
|
52
|
+
* Lo que no tiene forma de máquina no borra lo que ya había.
|
|
53
|
+
*/
|
|
54
|
+
note (pub, raw) {
|
|
55
|
+
if (typeof pub !== 'string' || !pub) throw fail('bad-pub', 'note: a device key is required')
|
|
56
|
+
const m = cleanMachine(raw)
|
|
57
|
+
if (!m) return false
|
|
58
|
+
if (sameMachine(items.get(pub), m)) return false
|
|
59
|
+
items.set(pub, { ...m, at: now() })
|
|
60
|
+
save()
|
|
61
|
+
return true
|
|
62
|
+
},
|
|
63
|
+
/** Lo último que dijo, o `null` si nunca dijo nada. */
|
|
64
|
+
of (pub) {
|
|
65
|
+
const m = items.get(pub)
|
|
66
|
+
return m ? { ...m } : null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/tui/app.js
CHANGED
|
@@ -41,6 +41,7 @@ import * as hw from '../hwkeys.js'
|
|
|
41
41
|
import { VERSION } from '../version.js'
|
|
42
42
|
import * as up from '../update.js'
|
|
43
43
|
import { DEVICE_CAPS } from '@dotrino/identity/acta'
|
|
44
|
+
import { machineLine } from '@dotrino/identity/machine'
|
|
44
45
|
|
|
45
46
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
46
47
|
|
|
@@ -158,6 +159,9 @@ function mergeMembersAndCerts (members, issued) {
|
|
|
158
159
|
// QUÉ CORRE ESE APARATO (CONVENCIONES §14). Lo apunta la bóveda cuando le habla, así
|
|
159
160
|
// que aquí solo hay que dejarlo pasar — que es justo lo que le faltaba a `addedAt`.
|
|
160
161
|
running: m.running || null,
|
|
162
|
+
// DÓNDE CORRE (`@dotrino/identity/machine`): lo último que dijo firmado o, si no, lo
|
|
163
|
+
// que dijo al emparejarse. Es lo que distingue tres navegadores que se llaman igual.
|
|
164
|
+
machine: m.machine || cert?.machine || null,
|
|
161
165
|
// SI LE LLEGAN LOS AVISOS, solo en quien aprueba (`undefined` = no aprueba, no se pinta).
|
|
162
166
|
notifiedAt: (m.caps || []).includes('approve') && 'notifiedAt' in m ? m.notifiedAt : undefined
|
|
163
167
|
}
|
|
@@ -473,6 +477,9 @@ function deviceRows (st, t) {
|
|
|
473
477
|
}
|
|
474
478
|
for (const d of devices) {
|
|
475
479
|
const label = d.label || t.muted(i.noLabel)
|
|
480
|
+
// Dónde corre (`node · Linux 7.0 · mi-pc`), pegado al nombre: es con lo que se reconoce.
|
|
481
|
+
const maquina = machineLine(d.machine)
|
|
482
|
+
const donde = maquina ? t.muted(` ${maquina}`) : ''
|
|
476
483
|
const vars = devVarsOf(st, d.sub).length
|
|
477
484
|
const debt = debtOf(st, d.sub)
|
|
478
485
|
const desde = d.addedAt ? t.muted(` ${i.deviceSince}:${fmtExp(d.addedAt)}`) : ''
|
|
@@ -511,7 +518,7 @@ function deviceRows (st, t) {
|
|
|
511
518
|
? t.muted(i.thisVault)
|
|
512
519
|
: t.muted('scope:' + shortScope(d.scope)) + ' ' + t.muted('exp:' + fmtExp(d.exp)))
|
|
513
520
|
const marca = conCambios.has(d.sub) ? t.bold('*') : ' '
|
|
514
|
-
rows.push({ text: `${marca}${t.bold(d.deviceId)} ${label}${desde} ${status}${extra}`, sel: true, meta: d })
|
|
521
|
+
rows.push({ text: `${marca}${t.bold(d.deviceId)} ${label}${donde}${desde} ${status}${extra}`, sel: true, meta: d })
|
|
515
522
|
}
|
|
516
523
|
const revoked = st.devices?.revoked || []
|
|
517
524
|
if (revoked.length) {
|
|
@@ -2504,7 +2511,11 @@ function pairingBody (st, t, cols, height) {
|
|
|
2504
2511
|
lines.push('')
|
|
2505
2512
|
lines.push(t.danger(i.pairWarning))
|
|
2506
2513
|
lines.push('')
|
|
2507
|
-
if (st.pending)
|
|
2514
|
+
if (st.pending) {
|
|
2515
|
+
lines.push(t.warn(i.pairConnected(st.pending.deviceId)))
|
|
2516
|
+
// Dónde dice que corre, para comprobar que es el aparato que tienes delante.
|
|
2517
|
+
if (machineLine(st.pending.machine)) lines.push(t.muted(' ' + machineLine(st.pending.machine)))
|
|
2518
|
+
}
|
|
2508
2519
|
else lines.push(t.muted(i.pairWaiting))
|
|
2509
2520
|
return lines
|
|
2510
2521
|
}
|
package/src/vault.js
CHANGED
|
@@ -31,6 +31,8 @@ import { openSecretsStore, assertVar, RECOVERY as RECOVERY_WRAP, PROFILE_OWNER }
|
|
|
31
31
|
import { openSubacta } from './subacta.js'
|
|
32
32
|
import { openBlocked } from './blocked.js'
|
|
33
33
|
import { openNotifiable } from './notifiable.js'
|
|
34
|
+
import { openMachines } from './machines.js'
|
|
35
|
+
import { machineLine } from '@dotrino/identity/machine'
|
|
34
36
|
import { openNotices } from './notices.js'
|
|
35
37
|
import { makeEphemeralKey, openSealed } from '../lib/src/sealed.js'
|
|
36
38
|
import { VERSION } from './version.js'
|
|
@@ -266,6 +268,8 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
266
268
|
const blocked = openBlocked({ dir, atRest: passwordsAtRest })
|
|
267
269
|
// Qué aprobadores dijeron que pueden recibir avisos (`src/notifiable.js`). No decide nada: se enseña.
|
|
268
270
|
const notifiable = openNotifiable({ dir, atRest: passwordsAtRest })
|
|
271
|
+
// Dónde corre cada aparato, según lo último que dijo firmado (`src/machines.js`). Se enseña.
|
|
272
|
+
const machines = openMachines({ dir, atRest: passwordsAtRest })
|
|
269
273
|
// Lo que hay que contarle a quien aprueba y no es un pedido (`src/notices.js`): hoy, «me actualicé».
|
|
270
274
|
const notices = openNotices({ dir, atRest: passwordsAtRest })
|
|
271
275
|
const readPasswordsFile = () => {
|
|
@@ -549,6 +553,8 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
549
553
|
}
|
|
550
554
|
/** El dictamen de un aparato, para enseñarlo o para pegárselo a un error. */
|
|
551
555
|
const versionDe = (pub) => versiones.get(pub) || null
|
|
556
|
+
/** Dónde corre un aparato: lo último que dijo firmado (`machines`) o, si no, lo del acta. */
|
|
557
|
+
const machineOf = (pub, record) => machines.of(pub) || (record?.members || []).find((m) => m.pub === pub)?.machine || null
|
|
552
558
|
|
|
553
559
|
// ----- LA SUBACTA: lo decidido que todavía no selló el acta (`src/subacta.js`) -----
|
|
554
560
|
//
|
|
@@ -612,6 +618,11 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
612
618
|
const verifyChain = async (args) => {
|
|
613
619
|
const chk = await verifyChainRaw(args)
|
|
614
620
|
if (chk.ok && args?.data?.v) apuntarVersion(chk.device, args.data.v)
|
|
621
|
+
// DÓNDE CORRE, por el mismo motivo: se apunta de lo firmado. Un fallo al guardarlo no
|
|
622
|
+
// puede tumbar el pedido —es un dato para la lista—, pero se dice.
|
|
623
|
+
if (chk.ok && args?.data?.machine) {
|
|
624
|
+
try { machines.note(chk.device, args.data.machine) } catch (e) { log(`[vault] could not record where a device runs: ${e.message}`) }
|
|
625
|
+
}
|
|
615
626
|
// UN BLOQUEADO NO PASA POR NINGÚN MOSTRADOR. Sigue en el acta —bloquear no la toca,
|
|
616
627
|
// porque la bóveda vive cerrada y eso lo decide un aprobador desde el teléfono—, así que
|
|
617
628
|
// el papel verifica; la puerta se cierra aquí, que es por donde entra todo, y con su
|
|
@@ -809,12 +820,13 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
809
820
|
markRotationDue(sub).catch((e) => log('[vault] could not rotate after the removal:', e.message))
|
|
810
821
|
},
|
|
811
822
|
defaultScope: [SCOPE.READ],
|
|
812
|
-
onChallenge ({ deviceId, scope }) {
|
|
823
|
+
onChallenge ({ deviceId, scope, machine }) {
|
|
813
824
|
log(`\n[vault] Un dispositivo quiere conectarse:`)
|
|
814
825
|
log(` deviceId: ${deviceId}`)
|
|
826
|
+
if (machine) log(` machine: ${machineLine(machine)}`)
|
|
815
827
|
log(` Ingresa el código que MUESTRA el dispositivo:`)
|
|
816
828
|
log(` dotrino-vault approve <código> (o rechaza: dotrino-vault reject ${deviceId})\n`)
|
|
817
|
-
try { onEnrollChallenge?.({ deviceId, scope }) } catch (_) {}
|
|
829
|
+
try { onEnrollChallenge?.({ deviceId, scope, machine: machine || null }) } catch (_) {}
|
|
818
830
|
}
|
|
819
831
|
})
|
|
820
832
|
|
|
@@ -1330,6 +1342,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1330
1342
|
const devices = await Promise.all(issued.map(async (x) => ({
|
|
1331
1343
|
deviceId: x.sub ? await deviceIdOf(x.sub) : null, sub: x.sub || null, label: x.label || '', scope: x.scope, exp: x.exp, nonce: x.nonce,
|
|
1332
1344
|
running: x.sub ? versionDe(x.sub) : null,
|
|
1345
|
+
machine: x.sub ? machineOf(x.sub, record) : null,
|
|
1333
1346
|
notifiedAt: x.sub ? notifiable.since(x.sub) : null
|
|
1334
1347
|
})))
|
|
1335
1348
|
// LA SUBACTA VIAJA CON EL ACTA. Es la mitad de «se comparte entre todos los
|
|
@@ -4371,6 +4384,8 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
4371
4384
|
members: (r?.members || []).map((m) => ({
|
|
4372
4385
|
...m,
|
|
4373
4386
|
running: versionDe(m.pub),
|
|
4387
|
+
// Dónde corre: lo último que dijo firmado y, si nunca habló, lo del acta.
|
|
4388
|
+
machine: machines.of(m.pub) || m.machine || null,
|
|
4374
4389
|
// Cuándo dijo que le llegan los avisos (null = no lo ha dicho, o caducó). Solo
|
|
4375
4390
|
// tiene sentido en quien aprueba, y es un dato para enseñar: no decide nada.
|
|
4376
4391
|
notifiedAt: notifiable.since(m.pub),
|