@dotrino/vault 0.1.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/LICENSE +21 -0
- package/README.md +66 -0
- package/package.json +35 -0
- package/src/index.js +229 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 seyacat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @dotrino/vault
|
|
2
|
+
|
|
3
|
+
Usa **este dispositivo (navegador) como bóveda/CA** del ecosistema Dotrino, sin un PC
|
|
4
|
+
con el daemon. Es la contraparte browser del daemon `dotrino-vault`: atiende el mismo
|
|
5
|
+
protocolo de enrolamiento endurecido por el proxy y firma certificados de delegación
|
|
6
|
+
`D ← P` (donde `P` es la identidad de este dispositivo, `@dotrino/identity`).
|
|
7
|
+
|
|
8
|
+
Pensado para que **cualquier app** del ecosistema (no solo la terminal) pueda ofrecer
|
|
9
|
+
"usar este dispositivo como bóveda".
|
|
10
|
+
|
|
11
|
+
## Uso
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { Identity } from '@dotrino/identity'
|
|
15
|
+
import { startDeviceVault } from '@dotrino/vault'
|
|
16
|
+
|
|
17
|
+
const identity = await Identity.connect()
|
|
18
|
+
const vault = await startDeviceVault(identity) // se conecta al proxy como P
|
|
19
|
+
|
|
20
|
+
// 1) Abrir un emparejamiento y mostrar el QR/JSON al dispositivo a enrolar:
|
|
21
|
+
const { qr } = vault.startPairing({ label: 'mi-agente' })
|
|
22
|
+
// El dispositivo (p. ej. @dotrino/identity#enrollDevice) consume `qr`, GENERA un
|
|
23
|
+
// código aleatorio y lo MUESTRA (no lo envía).
|
|
24
|
+
|
|
25
|
+
// 2) Cuando el dispositivo pide acceso, aparece en la lista de pendientes:
|
|
26
|
+
vault.onPendingChange(() => {
|
|
27
|
+
for (const { deviceId } of vault.listPending()) {
|
|
28
|
+
// Un humano LEE el código del dispositivo y lo TIPEA aquí:
|
|
29
|
+
// await vault.approve(deviceId, codigoTipeado)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// 3) Máquinas ya enroladas / revocar:
|
|
34
|
+
const machines = await vault.listMachines() // [{ sub, deviceId, label, exp, nonce, scope }]
|
|
35
|
+
// await vault.revoke(nonce)
|
|
36
|
+
|
|
37
|
+
vault.close()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Modelo de aprobación (seguro por diseño)
|
|
41
|
+
|
|
42
|
+
- El **dispositivo** que se enrola genera un **código aleatorio** (`makePairingCode`) y
|
|
43
|
+
lo **muestra**; el código **no viaja** por la red.
|
|
44
|
+
- Esta bóveda **no conoce** el código: un humano lo **lee del dispositivo** y lo **tipea**
|
|
45
|
+
aquí. Al aprobar, la bóveda firma el cert y **echa** el código tipeado de vuelta.
|
|
46
|
+
- El dispositivo acepta el cert **solo si el código echado coincide** con el que generó.
|
|
47
|
+
Así, una bóveda falsa (que nunca vio el código) no puede enrolarlo, y **aprobar a ciegas**
|
|
48
|
+
(sin ir a leer el código del dispositivo) no enrola a nadie.
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
`startDeviceVault(identity, { proxyUrl? }) → Promise<handle>`
|
|
53
|
+
|
|
54
|
+
- `startPairing({ scope?, ttlMs?, label? }) → { qr, expiresInMs }`
|
|
55
|
+
- `listPending() → [{ deviceId, label }]`
|
|
56
|
+
- `approve(deviceId, code) → Promise<{ ok, deviceId }>` (code = lo que muestra el dispositivo)
|
|
57
|
+
- `reject(deviceId)`
|
|
58
|
+
- `listMachines() → Promise<[{ sub, deviceId, label, scope, exp, nonce }]>`
|
|
59
|
+
- `revoke(nonce) → Promise`
|
|
60
|
+
- `getSelfCert() → Promise<cert>` (self-cert `P ← P`, para actuar además de cliente)
|
|
61
|
+
- `onPendingChange(fn)`, `close()`
|
|
62
|
+
|
|
63
|
+
Cripto y firma: `@dotrino/identity`. Transporte: `@dotrino/proxy-client`. No reimplementa
|
|
64
|
+
nada del ecosistema.
|
|
65
|
+
|
|
66
|
+
MIT · parte de [Dotrino](https://dotrino.com).
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dotrino/vault",
|
|
3
|
+
"version": "0.1.0",
|
|
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. Contraparte browser del daemon dotrino-vault, reutilizable por cualquier app.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"module": "src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"dotrino",
|
|
20
|
+
"vault",
|
|
21
|
+
"identity",
|
|
22
|
+
"delegation",
|
|
23
|
+
"pairing"
|
|
24
|
+
],
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@dotrino/identity": ">=0.17.0",
|
|
27
|
+
"@dotrino/proxy-client": ">=0.6.0"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/imdotrino/dotrino-vault.git",
|
|
33
|
+
"directory": "lib"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dotrino/vault — "este dispositivo es una bóveda" (lado SERVIDOR, browser+node).
|
|
3
|
+
*
|
|
4
|
+
* Convierte la identidad de ESTE dispositivo (`@dotrino/identity`, la clave P) en una
|
|
5
|
+
* bóveda/CA: atiende el MISMO protocolo de enrolamiento endurecido que el daemon
|
|
6
|
+
* `dotrino-vault` (`vault.enroll` → `vault.enroll.challenge` → `vault.enrolled`) por el
|
|
7
|
+
* proxy del ecosistema, firma certificados de delegación `D ← P` al aprobar, y responde
|
|
8
|
+
* consultas de revocación (`vault.devices`). Así CUALQUIER app (no solo la terminal)
|
|
9
|
+
* puede dejar que el usuario use su dispositivo como bóveda, sin un PC con el daemon.
|
|
10
|
+
*
|
|
11
|
+
* Modelo de aprobación SEGURO (idéntico al daemon `dotrino-vault#approveDevice`):
|
|
12
|
+
* - El DISPOSITIVO que se enrola (p. ej. `@dotrino/identity#enrollDevice`) genera un
|
|
13
|
+
* código ALEATORIO (`makePairingCode`) y lo MUESTRA; NO lo envía por la red.
|
|
14
|
+
* - Esta bóveda NO conoce el código: un humano lo LEE del dispositivo y lo TIPEA aquí.
|
|
15
|
+
* - Al aprobar, la bóveda firma el cert y ECHA el código tipeado de vuelta.
|
|
16
|
+
* - El dispositivo acepta el cert SOLO si el código echado coincide con el que generó.
|
|
17
|
+
* → una bóveda falsa (que nunca vio el código) no puede enrolar el dispositivo, y
|
|
18
|
+
* aprobar "a ciegas" (sin ir a leer el código del dispositivo) tampoco enrola nada.
|
|
19
|
+
*
|
|
20
|
+
* Cripto 100% de `@dotrino/identity/capabilities` (verifyDeviceSig/verifyChain/pubkeyId)
|
|
21
|
+
* + firma con la identidad P (`identity.signDelegation`). Transporte: `@dotrino/proxy-client`
|
|
22
|
+
* (import perezoso). No reimplementa nada del ecosistema.
|
|
23
|
+
*/
|
|
24
|
+
import { verifyDeviceSig, verifyChain, pubkeyId } from '@dotrino/identity/capabilities'
|
|
25
|
+
|
|
26
|
+
const SIGN_SCOPE = 'vault:sign'
|
|
27
|
+
const PAIRING_TTL_MS = 5 * 60 * 1000 // un emparejamiento (token) vale 5 min
|
|
28
|
+
const DEVICE_TTL_MS = 30 * 24 * 60 * 60 * 1000 // vida de un cert de dispositivo (30 días)
|
|
29
|
+
const SELFCERT_TTL_MS = 24 * 60 * 60 * 1000 // el self-cert P←P se regenera cada 24 h
|
|
30
|
+
const FRESH_WINDOW_MS = 5 * 60 * 1000 // ventana anti-replay del enroll (±5 min)
|
|
31
|
+
|
|
32
|
+
const MSG = {
|
|
33
|
+
ENROLL: 'vault.enroll',
|
|
34
|
+
ENROLL_CHALLENGE: 'vault.enroll.challenge',
|
|
35
|
+
ENROLLED: 'vault.enrolled',
|
|
36
|
+
DEVICES: 'vault.devices',
|
|
37
|
+
DEVICES_RESULT: 'vault.devices.result',
|
|
38
|
+
ERROR: 'vault.error'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function randToken () {
|
|
42
|
+
const b = crypto.getRandomValues(new Uint8Array(16))
|
|
43
|
+
return [...b].map((x) => x.toString(16).padStart(2, '0')).join('')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** deviceId legible (p. ej. `C440-AC0E`) desde una pubkey JWK. */
|
|
47
|
+
export function deviceIdOf (pub) {
|
|
48
|
+
return pubkeyId(pub).then((id) => id.slice(0, 8).toUpperCase().replace(/(.{4})(.{4})/, '$1-$2'))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Levanta la bóveda de este dispositivo: se conecta al proxy identificado como P y
|
|
53
|
+
* atiende enrolamientos + consultas de revocación de los dispositivos que se enrolan.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} identity instancia de `@dotrino/identity` (P): expone
|
|
56
|
+
* `me.publickey`, `signData`, `signDelegation`, `listDelegations`, `revokeDelegation`.
|
|
57
|
+
* @param {object} [opts]
|
|
58
|
+
* @param {string} [opts.proxyUrl='wss://proxy.dotrino.com']
|
|
59
|
+
* @returns {Promise<object>} handle: { iss, proxy, client, startPairing, approve, reject,
|
|
60
|
+
* listPending, listMachines, revoke, getSelfCert, onPendingChange, close }
|
|
61
|
+
*/
|
|
62
|
+
export async function startDeviceVault (identity, { proxyUrl } = {}) {
|
|
63
|
+
const iss = identity.me?.publickey
|
|
64
|
+
if (!iss) throw new Error('sin identidad: crea/desbloquea tu identidad antes de usar el dispositivo como bóveda')
|
|
65
|
+
const proxy = proxyUrl || 'wss://proxy.dotrino.com'
|
|
66
|
+
|
|
67
|
+
// ----- self-cert P ← P (para que este dispositivo pueda además actuar de cliente
|
|
68
|
+
// de sus propias máquinas: lo firma la propia P y verifyChain lo acepta) -----
|
|
69
|
+
let _selfCert = null
|
|
70
|
+
const getSelfCert = async () => {
|
|
71
|
+
if (_selfCert && _selfCert.exp > Date.now() + 60_000) return _selfCert
|
|
72
|
+
const { cert } = await identity.signDelegation(iss, SIGN_SCOPE, { ttlMs: SELFCERT_TTL_MS })
|
|
73
|
+
_selfCert = cert
|
|
74
|
+
return cert
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
|
|
78
|
+
const client = new WebSocketProxyClient({
|
|
79
|
+
url: proxy, enableWebRTC: false, autoReconnect: true,
|
|
80
|
+
maxReconnectAttempts: 100000, reconnectDelay: 4000
|
|
81
|
+
})
|
|
82
|
+
await client.connect()
|
|
83
|
+
|
|
84
|
+
const selfCert = await getSelfCert()
|
|
85
|
+
const identify = async () => {
|
|
86
|
+
if (!client.token) return
|
|
87
|
+
const data = { op: 'identify', publickey: iss, token: client.token, ts: Date.now() }
|
|
88
|
+
const { signature } = await identity.signData(data)
|
|
89
|
+
await client.identify({ data, signature, cert: selfCert })
|
|
90
|
+
}
|
|
91
|
+
await identify()
|
|
92
|
+
client.on('token', () => identify().catch(() => {}))
|
|
93
|
+
|
|
94
|
+
const send = (to, obj) => { try { client.send(to, obj) } catch (_) {} }
|
|
95
|
+
|
|
96
|
+
// token -> { exp, sn, scope, ttlMs, label, state, dpub?, deviceId?, from? }
|
|
97
|
+
const pending = new Map()
|
|
98
|
+
let _onPendingChange = () => {}
|
|
99
|
+
|
|
100
|
+
async function handleEnroll (from, p) {
|
|
101
|
+
const d = p?.data
|
|
102
|
+
if (!d || typeof d.dpub !== 'string' || typeof p.signature !== 'string') {
|
|
103
|
+
return send(from, { type: MSG.ERROR, error: 'enroll inválido' })
|
|
104
|
+
}
|
|
105
|
+
const pend = pending.get(d.token)
|
|
106
|
+
if (!pend || Date.now() > pend.exp) {
|
|
107
|
+
return send(from, { type: MSG.ERROR, error: 'token de emparejamiento inválido o expirado' })
|
|
108
|
+
}
|
|
109
|
+
if (d.sn !== pend.sn) return send(from, { type: MSG.ERROR, error: 'sesión inválida' })
|
|
110
|
+
if (typeof d.ts !== 'number' || Math.abs(Date.now() - d.ts) > FRESH_WINDOW_MS) {
|
|
111
|
+
return send(from, { type: MSG.ERROR, error: 'enroll vencido (posible replay, o el reloj desfasado)' })
|
|
112
|
+
}
|
|
113
|
+
// PRUEBA DE POSESIÓN: la firma de `data` debe verificar contra `dpub`.
|
|
114
|
+
const ok = await verifyDeviceSig({ publickey: d.dpub, data: d, signature: p.signature })
|
|
115
|
+
if (!ok) return send(from, { type: MSG.ERROR, error: 'firma de dispositivo inválida' })
|
|
116
|
+
// Un solo dispositivo a la vez esperando su código (así `approve` no es ambiguo).
|
|
117
|
+
if (pend.state === 'PENDING_CONFIRM' && pend.dpub && pend.dpub !== d.dpub) {
|
|
118
|
+
return send(from, { type: MSG.ERROR, error: 'ya hay un dispositivo usando este emparejamiento' })
|
|
119
|
+
}
|
|
120
|
+
const deviceId = await deviceIdOf(d.dpub)
|
|
121
|
+
pend.state = 'PENDING_CONFIRM'
|
|
122
|
+
pend.dpub = d.dpub
|
|
123
|
+
pend.deviceId = deviceId
|
|
124
|
+
pend.from = from // esta bóveda NO conoce el código (no viaja): el dispositivo lo MUESTRA
|
|
125
|
+
if (d.label) pend.label = String(d.label).slice(0, 60)
|
|
126
|
+
_onPendingChange()
|
|
127
|
+
send(from, { type: MSG.ENROLL_CHALLENGE, deviceId })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Consulta de revocaciones (igual que `vault.devices` del daemon): responde la lista
|
|
131
|
+
// de dispositivos enrolados + revocados para que el dispositivo refresque su set.
|
|
132
|
+
async function handleDevices (from, p) {
|
|
133
|
+
const d = p?.data
|
|
134
|
+
if (!d || !p.signature || !p.cert) return send(from, { type: MSG.ERROR, error: 'petición inválida' })
|
|
135
|
+
if (typeof d.ts !== 'number' || Math.abs(Date.now() - d.ts) > FRESH_WINDOW_MS) return
|
|
136
|
+
const chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, trustedIssuer: iss })
|
|
137
|
+
if (!chk.ok) return send(from, { type: MSG.ERROR, error: 'no autorizado: ' + chk.reason })
|
|
138
|
+
const { issued, revoked } = await identity.listDelegations()
|
|
139
|
+
const devices = await Promise.all((issued || []).map(async (x) => ({
|
|
140
|
+
deviceId: x.sub ? await deviceIdOf(x.sub) : null, sub: x.sub || null,
|
|
141
|
+
label: x.label || '', scope: x.scope, exp: x.exp, nonce: x.nonce
|
|
142
|
+
})))
|
|
143
|
+
send(from, { type: MSG.DEVICES_RESULT, devices, revoked: (revoked || []).map((r) => r.nonce || r) })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
client.on('message', (_from, p) => {
|
|
147
|
+
if (!p || typeof p !== 'object') return
|
|
148
|
+
if (p.type === MSG.ENROLL) handleEnroll(_from, p).catch(() => {})
|
|
149
|
+
else if (p.type === MSG.DEVICES) handleDevices(_from, p).catch(() => {})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Abre un emparejamiento: devuelve el QR/JSON v2 que el dispositivo consume para
|
|
154
|
+
* enrolarse. `scope`/`ttlMs`/`label` fijan lo que otorgará el cert al aprobar.
|
|
155
|
+
*/
|
|
156
|
+
function startPairing ({ scope = [SIGN_SCOPE], ttlMs = DEVICE_TTL_MS, label = '' } = {}) {
|
|
157
|
+
pending.clear()
|
|
158
|
+
const token = randToken()
|
|
159
|
+
const sn = randToken()
|
|
160
|
+
pending.set(token, { token, exp: Date.now() + PAIRING_TTL_MS, sn, scope, ttlMs, label, state: 'AWAITING_ENROLL' })
|
|
161
|
+
return { qr: { v: 2, iss, proxy, token, sn }, expiresInMs: PAIRING_TTL_MS }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function listPending () {
|
|
165
|
+
return [...pending.values()]
|
|
166
|
+
.filter((p) => p.state === 'PENDING_CONFIRM')
|
|
167
|
+
.map((p) => ({ deviceId: p.deviceId, label: p.label }))
|
|
168
|
+
}
|
|
169
|
+
function findPending (deviceId) {
|
|
170
|
+
for (const [, p] of pending) if (p.state === 'PENDING_CONFIRM' && p.deviceId === deviceId) return p
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Aprueba una máquina pendiente TIPEANDO el código que ella muestra. Esta bóveda NO
|
|
176
|
+
* conoce/valida el código: firma el cert y ECHA el código tipeado; la máquina lo acepta
|
|
177
|
+
* solo si coincide con el que generó. (Modelo `dotrino-vault#approveDevice`.)
|
|
178
|
+
*/
|
|
179
|
+
async function approve (deviceId, code) {
|
|
180
|
+
const pend = findPending(deviceId)
|
|
181
|
+
if (!pend || !pend.dpub) throw new Error('no hay ninguna máquina esperando aprobación')
|
|
182
|
+
code = String(code || '').trim()
|
|
183
|
+
if (!code) throw new Error('escribe el código que muestra la máquina')
|
|
184
|
+
const { cert } = await identity.signDelegation(pend.dpub, pend.scope, { ttlMs: pend.ttlMs, label: pend.label })
|
|
185
|
+
send(pend.from, { type: MSG.ENROLLED, code, cert, iss })
|
|
186
|
+
pending.delete(pend.token)
|
|
187
|
+
_onPendingChange()
|
|
188
|
+
return { ok: true, deviceId }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function reject (deviceId) {
|
|
192
|
+
const pend = findPending(deviceId)
|
|
193
|
+
if (!pend) return
|
|
194
|
+
send(pend.from, { type: MSG.ERROR, error: 'emparejamiento rechazado' })
|
|
195
|
+
pending.delete(pend.token)
|
|
196
|
+
_onPendingChange()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Máquinas enroladas bajo esta identidad (P), vigentes, con scope de firma y label
|
|
201
|
+
* propio (excluye navegadores enrolados con label 'cli', que no atienden peticiones).
|
|
202
|
+
*/
|
|
203
|
+
async function listMachines () {
|
|
204
|
+
const { issued } = await identity.listDelegations()
|
|
205
|
+
const now = Date.now()
|
|
206
|
+
const bySub = new Map()
|
|
207
|
+
for (const x of (issued || [])) {
|
|
208
|
+
if (!x.sub || (x.exp && x.exp <= now)) continue
|
|
209
|
+
if (!Array.isArray(x.scope) || !x.scope.includes(SIGN_SCOPE)) continue
|
|
210
|
+
if (!x.label || x.label === 'cli') continue
|
|
211
|
+
if (!bySub.has(x.sub) || (x.exp || 0) > (bySub.get(x.sub).exp || 0)) bySub.set(x.sub, x)
|
|
212
|
+
}
|
|
213
|
+
return Promise.all([...bySub.values()].map(async (x) => ({ ...x, deviceId: await deviceIdOf(x.sub) })))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function revoke (nonce) {
|
|
217
|
+
return identity.revokeDelegation(nonce)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
iss, proxy, client,
|
|
222
|
+
startPairing, approve, reject, listPending, listMachines, revoke,
|
|
223
|
+
getSelfCert,
|
|
224
|
+
onPendingChange (fn) { _onPendingChange = fn || (() => {}) },
|
|
225
|
+
close () { try { client.close() } catch (_) {} }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export default { startDeviceVault, deviceIdOf }
|