@dotrino/identity 0.115.0 → 0.116.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 +4 -1
- package/src/node.js +4 -180
- package/vault/core.js +66 -7
- package/vault/facade.js +200 -0
- package/vault/index.html +10 -0
- package/vault/remote.js +45 -5
- package/vault/vault.js +29 -58
- package/vault/vendor/compat/VERSION.txt +3 -0
- package/vault/vendor/compat/advisory.js +141 -0
- package/vault/vendor/compat/index.js +188 -0
- package/vault/vendor/compat/ranges.js +93 -0
- package/vault/vendor/opaque/VERSION.txt +1 -1
- package/vault/vendor/passmanager/VERSION.txt +3 -0
- package/vault/vendor/passmanager/crypto.js +167 -0
- package/vault/vendor/passmanager/fields.js +183 -0
- package/vault/vendor/passmanager/generate.js +74 -0
- package/vault/vendor/passmanager/import.js +156 -0
- package/vault/vendor/passmanager/index.js +27 -0
- package/vault/vendor/passmanager/match.js +137 -0
- package/vault/vendor/passmanager/model.js +332 -0
- package/vault/vendor/passmanager/pubkey.js +33 -0
- package/vault/vendor/passmanager/sealed/convert.js +72 -0
- package/vault/vendor/passmanager/sealed/device.js +211 -0
- package/vault/vendor/passmanager/sealed/entry.js +209 -0
- package/vault/vendor/passmanager/sealed/index.js +20 -0
- package/vault/vendor/passmanager/sealed/keys.js +120 -0
- package/vault/vendor/passmanager/sealed/recovery.js +136 -0
- package/vault/vendor/passmanager/sealed/store.js +508 -0
- package/vault/vendor/passmanager/session-cache.js +60 -0
- package/vault/vendor/passmanager/totp.js +111 -0
- package/vault/vendor/passmanager/transport/protocol.js +83 -0
- package/vault/vendor/passmanager/transport/proxy.js +112 -0
- package/vault/vendor/passmanager/transport/sealed.js +74 -0
- package/vault/vendor/passmanager/vault/approval.js +63 -0
- package/vault/vendor/passmanager/vault/errors.js +34 -0
- package/vault/vendor/passmanager/vault/guard.js +114 -0
- package/vault/vendor/passmanager/vault/interface.js +34 -0
- package/vault/vendor/passmanager/vault/local.js +213 -0
- package/vault/vendor/passmanager/vault/remote.js +85 -0
- package/vault/vendor/passmanager/vault/responder.js +192 -0
- package/vault/vendor/passmanager/vault/sealed-local.js +199 -0
- package/vault/vendor/passmanager/vault/sealed-responder.js +159 -0
- package/vault/vendor/passmanager/vault/sealed.js +362 -0
- package/vault/vendor/passmanager/webauthn.js +221 -0
- package/vault/vendor/proxy-client/VERSION.txt +1 -1
- package/vault/vendor/proxy-client/client.js +36 -0
- package/vault/vendor/roadmap/VERSION.txt +3 -0
- package/vault/vendor/roadmap/index.js +108 -0
- package/vault/vendor/roadmap/manifests.js +797 -0
- package/vault/vendor/store/VERSION.txt +3 -0
- package/vault/vendor/store/core.js +309 -0
- package/vault/vendor/vault/VERSION.txt +4 -1
- package/vault/vendor/vault/admin.js +437 -0
- package/vault/vendor/vault/browser.js +167 -0
- package/vault/vendor/vault/core/approvals.js +204 -0
- package/vault/vendor/vault/core/blocked.js +68 -0
- package/vault/vendor/vault/core/browserDisk.js +109 -0
- package/vault/vendor/vault/core/commKey.js +68 -0
- package/vault/vendor/vault/core/disk.js +65 -0
- package/vault/vendor/vault/core/libVersion.js +8 -0
- package/vault/vendor/vault/core/machines.js +66 -0
- package/vault/vendor/vault/core/notices.js +71 -0
- package/vault/vendor/vault/core/notifiable.js +73 -0
- package/vault/vendor/vault/core/passwords.js +125 -0
- package/vault/vendor/vault/core/sealKey.js +77 -0
- package/vault/vendor/vault/core/sealer.js +172 -0
- package/vault/vendor/vault/core/sealers.js +159 -0
- package/vault/vendor/vault/core/secretsStore.js +1326 -0
- package/vault/vendor/vault/core/sha256.js +69 -0
- package/vault/vendor/vault/core/store.js +68 -0
- package/vault/vendor/vault/core/subacta.js +101 -0
- package/vault/vendor/vault/core/threadStore.js +273 -0
- package/vault/vendor/vault/core/transport.js +270 -0
- package/vault/vendor/vault/core/vault.js +4414 -0
- package/vault/vendor/vault/enroll.js +8 -4
- package/vault/vendor/vault/procContext.js +97 -0
- package/vault/vendor/vault/revocation.js +67 -0
- package/vault/vendor/vault/sealed.js +84 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin.js — CONSOLA REMOTA: administrar el perfil desde un dispositivo emparejado.
|
|
3
|
+
*
|
|
4
|
+
* Diseño: `dotrino-vault/docs/consola-remota.md`. Módulo PURO (sin `node:*`, sin red,
|
|
5
|
+
* sin disco), igual que `enroll.js`, para que lo puedan usar el daemon del PC y «este
|
|
6
|
+
* dispositivo es bóveda» sin duplicar la regla.
|
|
7
|
+
*
|
|
8
|
+
* QUÉ SE DELEGA Y QUÉ NO — esto es el módulo entero, el resto es plomería:
|
|
9
|
+
*
|
|
10
|
+
* sí · ver el acta y la bitácora · REVOCAR a un miembro
|
|
11
|
+
* · VARIABLES DE ENTORNO: crearlas y darles valor (de un scope o de un aparato),
|
|
12
|
+
* y ver el valor de las marcadas PÚBLICAS
|
|
13
|
+
* no · AGREGAR UN APARATO (quitado 2026-08-31, ver abajo)
|
|
14
|
+
* · cambiar permisos · traspasar el mando · conceder `admin`
|
|
15
|
+
* · ver el valor de una variable PRIVADA · borrar variables
|
|
16
|
+
* · NADA que cambie algo mientras el perfil está CERRADO (ver
|
|
17
|
+
* `ADMIN_OPS_WHILE_LOCKED`): el candado es de la consola, y esto es la consola.
|
|
18
|
+
*
|
|
19
|
+
* SOBRE LAS VARIABLES, que es la rendija más nueva: lo que cruza la frontera no es «los
|
|
20
|
+
* secretos» sino los que su dueño MARCÓ como mostrables. Una privada se puede reescribir
|
|
21
|
+
* a ciegas desde la consola, pero su valor no sale de la máquina de la bóveda ni para un
|
|
22
|
+
* aparato tuyo con `admin`. Y los valores que sí salen viajan CIFRADOS con la clave de
|
|
23
|
+
* contenido del perfil (quien llama sella y abre): el proxy transporta y no ve nada.
|
|
24
|
+
*
|
|
25
|
+
* La frontera no es un capricho: un admin puede **admitir y expulsar**, pero no
|
|
26
|
+
* reescribir quién manda. Así un aparato con `admin` robado hace daño **acotado y
|
|
27
|
+
* reversible** (se le revoca) en vez de poder traspasarse el mando y dejar al dueño
|
|
28
|
+
* fuera de su propia cuenta, que no tiene vuelta atrás. Las operaciones que no se
|
|
29
|
+
* delegan **no existen como mensaje**: no hay nada que autorizar mal.
|
|
30
|
+
*
|
|
31
|
+
* POR QUÉ AGREGAR APARATOS YA NO SE HACE DESDE AQUÍ (dueño, 2026-08-31). Existía para
|
|
32
|
+
* ahorrarse ir hasta la máquina de la bóveda. El **multivault** quita esa fricción de raíz
|
|
33
|
+
* —cualquier otra selladora abierta admite el aparato— así que esto dejó de comprar nada y
|
|
34
|
+
* se quedaba pidiendo lo caro: que la maestra pudiera firmar a distancia.
|
|
35
|
+
*
|
|
36
|
+
* Y era incompatible con la regla dura del acta: la maestra solo sella el acta y reenvuelve
|
|
37
|
+
* sobres, y con la bóveda cerrada no firma nada. Admitir a alguien es emitir un certificado
|
|
38
|
+
* Y sellar el acta: las dos cosas son de una selladora abierta, y esta consola no lo es.
|
|
39
|
+
*
|
|
40
|
+
* **Quitado de raíz, no escondido**: no existe la operación, ni el mensaje, ni el botón.
|
|
41
|
+
* Una funcionalidad escondida sigue siendo superficie de ataque y vuelve sola en el
|
|
42
|
+
* siguiente refactor.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/** Las únicas operaciones que existen a distancia. Lista cerrada, como las capacidades. */
|
|
46
|
+
export const ADMIN_OPS = Object.freeze([
|
|
47
|
+
// `pending`, `pair`, `approve` y `reject` NO están, y no es un olvido: agregar aparatos
|
|
48
|
+
// a distancia se quitó (ver la cabecera). Lo que queda es mirar, quitar y configurar.
|
|
49
|
+
'revoke', 'audit',
|
|
50
|
+
// Los BLOQUEADOS por un aprobador (`src/blocked.js`): verlos y desbloquearlos. Bloquear a
|
|
51
|
+
// distancia también, por si el administrador llega antes que el teléfono. No toca el acta.
|
|
52
|
+
'blocked', 'block', 'unblock',
|
|
53
|
+
// Variables de entorno: verlas (nombres siempre; valor solo de las públicas) y
|
|
54
|
+
// ponerles valor (de las dos). Borrar NO está, y es a propósito: un aparato robado
|
|
55
|
+
// no debe poder dejar sin configuración a los servicios.
|
|
56
|
+
// `var.setMany` es la MISMA operación con varias variables dentro de un solo sobre: no
|
|
57
|
+
// añade permisos, quita reinicios (ver el enrutado abajo).
|
|
58
|
+
// Ver un valor, su histórico o volver a una versión NO están, y es a propósito: un
|
|
59
|
+
// aparato que administra no tiene sobres de lo privado (solo el servicio dueño y la
|
|
60
|
+
// recuperación). Eso lo hace la bóveda en su máquina (`secret show/history/revert`).
|
|
61
|
+
// No lo cablees.
|
|
62
|
+
// `var.recipients` es solo LEER, y hace falta para poder escribir: la consola sella el
|
|
63
|
+
// valor ella misma (§8.1) y para eso necesita saber a quién envolverle la llave. Estaba
|
|
64
|
+
// documentada en `buildSealedVar` y la consola la llamaba, pero no existía — así que
|
|
65
|
+
// crear una variable desde la consola contestaba «admin: invalid operation». No expone
|
|
66
|
+
// nada nuevo: son llaves PÚBLICAS, las mismas que ya viajan en el acta.
|
|
67
|
+
'vars', 'var.set', 'var.setMany', 'var.recipients',
|
|
68
|
+
// ABRIR LA BÓVEDA A DISTANCIA (`docs/abrir-a-distancia.md`). Dos pasos: `unlock.begin`
|
|
69
|
+
// pide una llave efímera y los parámetros del molino; `unlock` trae la derivada dentro
|
|
70
|
+
// de un sobre que solo esta bóveda puede abrir. No hay una tercera.
|
|
71
|
+
'unlock.begin', 'unlock'
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* QUÉ SE ATIENDE CON EL CANDADO ECHADO.
|
|
76
|
+
*
|
|
77
|
+
* No es «solo mirar», y confundirlo fue un error: **guardar una variable NO necesita la
|
|
78
|
+
* maestra**. Sellar un valor usa las llaves PÚBLICAS de quien va a leerlo, así que la
|
|
79
|
+
* bóveda puede hacerlo sin ningún secreto suyo (`docs/secretos-sellados.md` §8.1) — es
|
|
80
|
+
* justo lo que hace barata la rotación. Meterlo en el mismo saco que revocar dejaba la
|
|
81
|
+
* consola sin poder configurar nada con el perfil cerrado, que es su trabajo diario.
|
|
82
|
+
*
|
|
83
|
+
* Lo que SÍ se queda fuera es `revoke`: reescribe el acta, y sellar el acta es de la
|
|
84
|
+
* maestra. Cerrada no hay con qué.
|
|
85
|
+
*
|
|
86
|
+
* (Si el cajón tiene su llave sellada con la frase, repartirla a los miembros puede quedar
|
|
87
|
+
* a deber hasta que alguien abra: eso ya lo dice la bóveda y se salda solo. Guardar el
|
|
88
|
+
* valor, que es lo que se pidió, se hace igual.)
|
|
89
|
+
*
|
|
90
|
+
* El candado ES de la consola —los aparatos ya emparejados siguen leyendo, guardando y
|
|
91
|
+
* firmando con SU llave— así que cerrar el perfil tiene que cerrar exactamente esto. No
|
|
92
|
+
* estaba: `admin.pair` y `admin.approve` pasaban con el perfil bloqueado, y `approve`
|
|
93
|
+
* firmaba un certificado de 30 días CON LA MAESTRA. Esas dos ya no existen —agregar
|
|
94
|
+
* aparatos se quitó de aquí—, pero la regla se queda para las que quedan: revocar reescribe
|
|
95
|
+
* el acta y configurar toca los secretos.
|
|
96
|
+
*
|
|
97
|
+
* Se dejan las de LEER a propósito: si se cortaran todas, la consola no podría ni
|
|
98
|
+
* enterarse de por qué no funciona y parecería rota. Así puede decir «está cerrada».
|
|
99
|
+
*/
|
|
100
|
+
export const ADMIN_OPS_WHILE_LOCKED = Object.freeze([
|
|
101
|
+
'audit', 'vars', 'var.set', 'var.setMany', 'var.recipients',
|
|
102
|
+
// Bloquear no sella nada: la lista es de la bóveda, no del acta.
|
|
103
|
+
'blocked', 'block', 'unblock',
|
|
104
|
+
// Las dos de abrir, obviamente: son las únicas que solo tienen sentido con el candado
|
|
105
|
+
// echado. Que estén aquí NO las deja pasar sin más — siguen exigiendo cert, acta, nonce
|
|
106
|
+
// y frescura como todas, y además la contraseña dentro del sobre.
|
|
107
|
+
'unlock.begin', 'unlock'
|
|
108
|
+
])
|
|
109
|
+
|
|
110
|
+
/** Cuánto se recuerda un nonce ya usado (el doble de la ventana de frescura). */
|
|
111
|
+
export const ADMIN_NONCE_TTL_MS = 10 * 60 * 1000
|
|
112
|
+
|
|
113
|
+
/** Tope de entradas de bitácora por petición. */
|
|
114
|
+
export const AUDIT_MAX = 500
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {Object} o
|
|
118
|
+
* @param {Object} o.desk `createEnrollDesk`, del que aquí solo se usa REVOCAR
|
|
119
|
+
* (`revoke`/`revokeDevice`). Emparejar y aprobar ya no se atienden a distancia.
|
|
120
|
+
* @param {(scope:string[])=>Promise<any>} o.verify verifica cadena+cert; devuelve `{ok, device, reason}`.
|
|
121
|
+
* @param {(limit:number)=>any[]} o.readActivity últimas entradas de la bitácora.
|
|
122
|
+
* @param {(pub:string)=>Promise<string>} o.deviceIdOf
|
|
123
|
+
* @param {{list:(a:object)=>Promise<any>, set:(a:object)=>Promise<any>, setMany?:(a:object)=>Promise<any>}} [o.vars]
|
|
124
|
+
* mostrador de VARIABLES DE ENTORNO. Va inyectado porque aquí no hay cripto ni disco:
|
|
125
|
+
* quien lo implementa (la bóveda) es quien sella con la clave de contenido del perfil y
|
|
126
|
+
* quien decide qué valor puede salir. Sin él, las ops de variables responden que esta
|
|
127
|
+
* bóveda no las atiende, en vez de fingir que se aplicaron.
|
|
128
|
+
* @param {(ev:string, info?:object)=>Promise<void>} [o.notify] aviso a todos los miembros.
|
|
129
|
+
* @param {(op:string, info?:object)=>void} [o.audit]
|
|
130
|
+
* @param {()=>boolean} [o.isLocked] candado del perfil. Cerrado, solo se atiende
|
|
131
|
+
* `ADMIN_OPS_WHILE_LOCKED`; lo demás responde con el código `vault-locked`.
|
|
132
|
+
*/
|
|
133
|
+
/**
|
|
134
|
+
* EL CUERPO QUE FIRMA EL AUTOR DE UN SOBRE. Uno solo, para los dos lados.
|
|
135
|
+
*
|
|
136
|
+
* Quien escribe firma esto y la bóveda verifica esto MISMO. Si cada lado se armara su
|
|
137
|
+
* versión, la firma dejaría de cuadrar el día que alguien añada un campo —y el síntoma
|
|
138
|
+
* sería «la firma no verifica», que manda a mirar la cripto en vez del formato.
|
|
139
|
+
*
|
|
140
|
+
* Lleva el CAJÓN y la CLAVE, no solo el sobre: sin ellos, una firma válida para
|
|
141
|
+
* `ns:aws/TOKEN` valdría igual para `ns:proxy/TOKEN`, que es exactamente el sobre colado
|
|
142
|
+
* fuera de su sitio que esto viene a impedir.
|
|
143
|
+
*/
|
|
144
|
+
export function authorBody (owner, key, e, ts) {
|
|
145
|
+
return { op: 'var.author', owner, key, iv: e?.iv || null, ct: e?.ct || null, ts }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createAdminDesk ({
|
|
149
|
+
desk, verify, readActivity = () => [], deviceIdOf, vars = null,
|
|
150
|
+
notify = async () => {}, audit = () => {}, isLocked = () => false,
|
|
151
|
+
// ABRIR A DISTANCIA. Va inyectado —como `vars`— porque abrir necesita cripto y disco, y
|
|
152
|
+
// este módulo es puro a propósito. `null` en una bóveda que no lo soporte: entonces la
|
|
153
|
+
// operación existe y contesta que no, en vez de romper con un TypeError.
|
|
154
|
+
unlockDesk = null,
|
|
155
|
+
// La lista de bloqueados, inyectada como `vars`: `{ list(), set(pub, on, { by }) }`.
|
|
156
|
+
blocked = null,
|
|
157
|
+
now = () => Date.now()
|
|
158
|
+
}) {
|
|
159
|
+
const ops = new Set(ADMIN_OPS)
|
|
160
|
+
const mientrasCerrada = new Set(ADMIN_OPS_WHILE_LOCKED)
|
|
161
|
+
|
|
162
|
+
// NONCE de un solo uso. `sign`/`get` son idempotentes y les basta la ventana de ±5
|
|
163
|
+
// min; `revoke` y `var.set` CAMBIAN ESTADO, así que reproducir uno dentro de esa
|
|
164
|
+
// ventana sí importa (re-aprobar un enrolamiento que el dueño ya rechazó, por
|
|
165
|
+
// ejemplo). Por eso el nonce, y por eso es obligatorio en todas las ops: una lista
|
|
166
|
+
// de excepciones es una invitación a equivocarse.
|
|
167
|
+
const seen = new Map()
|
|
168
|
+
function nonceAlreadyUsed (nonce) {
|
|
169
|
+
const t = now()
|
|
170
|
+
for (const [n, exp] of seen) if (exp <= t) seen.delete(n)
|
|
171
|
+
if (seen.has(nonce)) return true
|
|
172
|
+
seen.set(nonce, t + ADMIN_NONCE_TTL_MS)
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Atiende una petición ya verificada como *fresca*. Devuelve `{ ok, result }` o
|
|
178
|
+
* `{ ok: false, error }` — quien llama se encarga de responder por el transporte.
|
|
179
|
+
*/
|
|
180
|
+
async function handle (data, { signature, cert } = {}) {
|
|
181
|
+
if (!data || !ops.has(data.op)) return { ok: false, error: 'admin: invalid operation' }
|
|
182
|
+
if (typeof data.nonce !== 'string' || data.nonce.length < 16) {
|
|
183
|
+
return { ok: false, error: 'admin: missing single-use nonce' }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const chk = await verify({ data, signature, cert })
|
|
187
|
+
if (!chk?.ok) {
|
|
188
|
+
audit('rejected', { what: 'admin', op: data.op, reason: chk?.reason })
|
|
189
|
+
return { ok: false, error: 'unauthorized: ' + (chk?.reason || 'cert') }
|
|
190
|
+
}
|
|
191
|
+
const by = await deviceIdOf(chk.device).catch(() => null)
|
|
192
|
+
|
|
193
|
+
// CERRADA NO SE SELLA EL ACTA. Va después de autorizar (el candado es estado del perfil,
|
|
194
|
+
// y a un desconocido no se le cuenta) y ANTES de quemar el nonce: quien reintente
|
|
195
|
+
// tras abrir la bóveda no tiene por qué generar otro.
|
|
196
|
+
//
|
|
197
|
+
// El código va aparte del texto porque el texto se traduce y el código no: quien
|
|
198
|
+
// llama distingue «está cerrada, ábrela» de «no tienes permiso» sin leer la frase.
|
|
199
|
+
if (isLocked() && !mientrasCerrada.has(data.op)) {
|
|
200
|
+
audit('rejected', { what: 'admin', op: data.op, by, reason: 'locked' })
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
code: 'vault-locked',
|
|
204
|
+
error: 'vault locked: unlock the profile on the vault machine (dotrino-vault unlock) to administer it'
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// El nonce se marca DESPUÉS de autorizar: si no, cualquiera podría quemarle los
|
|
209
|
+
// nonces a un admin legítimo mandando basura firmada por nadie.
|
|
210
|
+
if (nonceAlreadyUsed(data.nonce)) {
|
|
211
|
+
audit('rejected', { what: 'admin', op: data.op, by, reason: 'replay' })
|
|
212
|
+
return { ok: false, error: 'admin: nonce already used' }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
|
|
217
|
+
// ABRIR A DISTANCIA. La bóveda hace el trabajo; aquí solo se enruta, porque este
|
|
218
|
+
// módulo es puro —sin cripto, sin disco— y abrir necesita las dos cosas.
|
|
219
|
+
if (data.op === 'unlock.begin') {
|
|
220
|
+
if (!unlockDesk) return { ok: false, error: 'admin: this vault cannot be opened remotely (update it)' }
|
|
221
|
+
return { ok: true, result: await unlockDesk.begin({ by, device: chk.device }) }
|
|
222
|
+
}
|
|
223
|
+
if (data.op === 'unlock') {
|
|
224
|
+
if (!unlockDesk) return { ok: false, error: 'admin: this vault cannot be opened remotely (update it)' }
|
|
225
|
+
return await unlockDesk.open({ nonce: data.nonce, enc: data.enc, by, device: chk.device })
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (data.op === 'audit') {
|
|
229
|
+
const limit = Math.min(Math.max(Number(data.limit) || 100, 1), AUDIT_MAX)
|
|
230
|
+
return { ok: true, result: { entries: readActivity(limit) } }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// VARIABLES DE ENTORNO. El módulo solo enruta y audita: qué valor puede salir y con
|
|
234
|
+
// qué se cifra lo decide la bóveda (`vars`), que es la que tiene la clave y el disco.
|
|
235
|
+
if (data.op === 'vars' || data.op === 'var.set' || data.op === 'var.setMany' || data.op === 'var.recipients') {
|
|
236
|
+
if (!vars) return { ok: false, error: 'admin: this vault does not serve environment variables' }
|
|
237
|
+
// Un destino y solo uno: o un scope, o un aparato. Sin esto, mandar los dos dejaría
|
|
238
|
+
// que quien llama adivine dónde acabó su variable.
|
|
239
|
+
// Booleanos a propósito: comparar los VALORES («proxy» vs una pubkey) nunca da
|
|
240
|
+
// igual, así que mandar los dos destinos se colaba por el hueco.
|
|
241
|
+
const toScope = typeof data.ns === 'string' && !!data.ns
|
|
242
|
+
const toDevice = typeof data.pub === 'string' && !!data.pub
|
|
243
|
+
if (data.op === 'vars') {
|
|
244
|
+
// QUIÉN PREGUNTA, no solo su nombre: con su llave se le manda la envoltura de
|
|
245
|
+
// cada pública para que pueda abrirla en su casa.
|
|
246
|
+
const result = await vars.list({ by, caller: chk.device })
|
|
247
|
+
audit('admin.vars', { by })
|
|
248
|
+
return { ok: true, result }
|
|
249
|
+
}
|
|
250
|
+
if (toScope === toDevice) return { ok: false, error: 'admin: var.set needs exactly one target (ns or pub)' }
|
|
251
|
+
|
|
252
|
+
// A QUIÉN HAY QUE ENVOLVERLE. Se pide ANTES de sellar, porque sellar lo hace la
|
|
253
|
+
// consola: envolver solo necesita públicas, así que puede hacerlo un navegador sin
|
|
254
|
+
// que ninguna privada ande suelta (§8.1). La lista sale del ACTA y la sabe la
|
|
255
|
+
// bóveda, no la consola — así conceder un permiso a un aparato nuevo basta, sin que
|
|
256
|
+
// nadie tenga que acordarse de actualizar una lista en otro sitio.
|
|
257
|
+
//
|
|
258
|
+
// Depende de la VISIBILIDAD: una pública se envuelve además para quien administra,
|
|
259
|
+
// así que quien pregunta dice cuál va a escribir.
|
|
260
|
+
if (data.op === 'var.recipients') {
|
|
261
|
+
if (typeof vars.recipients !== 'function') {
|
|
262
|
+
return { ok: false, error: 'admin: this vault cannot say who to seal for (update it)' }
|
|
263
|
+
}
|
|
264
|
+
const result = await vars.recipients({
|
|
265
|
+
ns: toScope ? data.ns : null,
|
|
266
|
+
pub: toDevice ? data.pub : null,
|
|
267
|
+
public: data.public === true
|
|
268
|
+
})
|
|
269
|
+
audit('admin.var.recipients', { by, ns: toScope ? data.ns : null })
|
|
270
|
+
return { ok: true, result }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// VARIAS DE UNA VEZ. Mismo permiso y misma frontera que una sola: lo que cambia es
|
|
274
|
+
// que la bóveda las guarda juntas y manda UN aviso de cambio en vez de uno por
|
|
275
|
+
// variable — o sea, el servicio se reinicia una vez, con la configuración entera,
|
|
276
|
+
// en lugar de arrancar a medias mientras quien administra sigue escribiendo.
|
|
277
|
+
// Los nombres viajan DENTRO del sobre, igual que los valores: el proxy tampoco
|
|
278
|
+
// tiene por qué aprender cómo se llaman las variables de un servicio.
|
|
279
|
+
if (data.op === 'var.setMany') {
|
|
280
|
+
// Una bóveda anterior a esto sabe guardar de una en una y nada más. Decirlo es
|
|
281
|
+
// mejor que reventar con un TypeError que no explica qué falta actualizar.
|
|
282
|
+
if (typeof vars.setMany !== 'function') return { ok: false, error: 'admin: this vault cannot save several variables at once (update it)' }
|
|
283
|
+
if (!Array.isArray(data.items) || !data.items.length) {
|
|
284
|
+
return { ok: false, error: 'admin: var.setMany needs `items`, each with its key and its sealed envelope' }
|
|
285
|
+
}
|
|
286
|
+
const result = await vars.setMany({
|
|
287
|
+
ns: toScope ? data.ns : null,
|
|
288
|
+
pub: toDevice ? data.pub : null,
|
|
289
|
+
items: data.items,
|
|
290
|
+
caller: chk.device,
|
|
291
|
+
by
|
|
292
|
+
})
|
|
293
|
+
const keys = result?.keys || []
|
|
294
|
+
// EN ESPERA DE APROBACIÓN: todavía no cambió nada, así que no hay de qué avisar. El
|
|
295
|
+
// aviso sale cuando se aprueba y se escribe de verdad.
|
|
296
|
+
if (result?.pending) {
|
|
297
|
+
audit('admin.var.pending', { by, ns: toScope ? data.ns : null, id: result.pending, count: data.items.length })
|
|
298
|
+
return { ok: true, result }
|
|
299
|
+
}
|
|
300
|
+
audit('admin.var.set', { by, ns: toScope ? data.ns : null, device: toDevice ? await deviceIdOf(data.pub).catch(() => null) : null, keys })
|
|
301
|
+
await notify('vars', { by, keys, ns: toScope ? data.ns : null })
|
|
302
|
+
return { ok: true, result: result || { ok: true } }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (typeof data.key !== 'string' || !data.key) return { ok: false, error: 'admin: var.set needs a key' }
|
|
306
|
+
if (!data.sealed || typeof data.sealed !== 'object') {
|
|
307
|
+
// El valor NUNCA viaja en claro, y desde 2026-09-02 tampoco sellado al perfil: el
|
|
308
|
+
// sobre llega HECHO y la bóveda no lo abre. Si llega de otra forma es un error de
|
|
309
|
+
// quien llama, no algo que se pueda «arreglar» aceptándolo.
|
|
310
|
+
return { ok: false, error: 'admin: var.set needs the value already sealed (`sealed`); build it with buildSealedVar()' }
|
|
311
|
+
}
|
|
312
|
+
const result = await vars.set({
|
|
313
|
+
ns: toScope ? data.ns : null,
|
|
314
|
+
pub: toDevice ? data.pub : null,
|
|
315
|
+
key: data.key,
|
|
316
|
+
sealed: data.sealed,
|
|
317
|
+
public: typeof data.public === 'boolean' ? data.public : undefined,
|
|
318
|
+
// LA LLAVE DE QUIEN LLAMA, no solo su nombre corto. `by` es el `deviceId`, que
|
|
319
|
+
// vale para el registro pero no para comprobar una firma: el sobre trae la de su
|
|
320
|
+
// AUTOR y hay que verificarla contra la llave de quien lo manda.
|
|
321
|
+
caller: chk.device,
|
|
322
|
+
by
|
|
323
|
+
})
|
|
324
|
+
if (result?.pending) {
|
|
325
|
+
audit('admin.var.pending', { by, ns: toScope ? data.ns : null, id: result.pending, count: 1 })
|
|
326
|
+
return { ok: true, result }
|
|
327
|
+
}
|
|
328
|
+
audit('admin.var.set', { by, ns: toScope ? data.ns : null, device: toDevice ? await deviceIdOf(data.pub).catch(() => null) : null, key: data.key })
|
|
329
|
+
// Cambiar la configuración de un servicio a distancia no puede ser invisible: es
|
|
330
|
+
// la contrapartida de delegar (F3 de docs/consola-remota.md).
|
|
331
|
+
await notify('vars', { by, key: data.key, ns: toScope ? data.ns : null })
|
|
332
|
+
return { ok: true, result: result || { ok: true } }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// QUITAR UN DISPOSITIVO se hace por `sub` (su llave): sale del acta Y se le retiran
|
|
336
|
+
// todos los certificados. Las dos cosas o ninguna.
|
|
337
|
+
//
|
|
338
|
+
// `certNonce` retira UN PAPEL y nada más — el aparato sigue siendo miembro. Sirve
|
|
339
|
+
// para eso y solo para eso, y se conserva por compatibilidad, pero no es «quitar»:
|
|
340
|
+
// usarlo para quitar dejaba un miembro sin certificados al que la bóveda ya nunca le
|
|
341
|
+
// mandaba el aviso de expulsión (mientras siga en el acta, un papel retirado
|
|
342
|
+
// significa «renueva»). Ese era el dispositivo fantasma.
|
|
343
|
+
if (data.op === 'blocked' || data.op === 'block' || data.op === 'unblock') {
|
|
344
|
+
if (!blocked) return { ok: false, error: 'admin: this vault cannot block devices' }
|
|
345
|
+
if (data.op === 'blocked') return { ok: true, result: { items: blocked.list() } }
|
|
346
|
+
if (typeof data.sub !== 'string' || !data.sub) return { ok: false, error: `admin: ${data.op} needs the device key (sub)` }
|
|
347
|
+
const r = await blocked.set(data.sub, data.op === 'block', { by })
|
|
348
|
+
audit(`admin.${data.op}`, { by, device: await deviceIdOf(data.sub).catch(() => null) })
|
|
349
|
+
return { ok: true, result: { ok: true, was: r } }
|
|
350
|
+
}
|
|
351
|
+
if (data.op === 'revoke') {
|
|
352
|
+
if (data.sub) {
|
|
353
|
+
const deviceId = await deviceIdOf(String(data.sub)).catch(() => null)
|
|
354
|
+
const r = await desk.revokeDevice(String(data.sub))
|
|
355
|
+
audit('admin.revoke-device', { by, device: deviceId, certs: r?.nonces?.length ?? null })
|
|
356
|
+
// Con el `deviceId`: el aviso a los demás dispositivos tiene que decir a QUIÉN
|
|
357
|
+
// quitaron, o no se puede saber si el que sobra eres tú.
|
|
358
|
+
await notify('revoked', { deviceId, by })
|
|
359
|
+
return { ok: true, result: r || { ok: true } }
|
|
360
|
+
}
|
|
361
|
+
const r = await desk.revoke(String(data.certNonce || ''))
|
|
362
|
+
audit('admin.revoke', { by, certNonce: data.certNonce })
|
|
363
|
+
await notify('revoked', { certNonce: data.certNonce, by })
|
|
364
|
+
return { ok: true, result: r || { ok: true } }
|
|
365
|
+
}
|
|
366
|
+
} catch (e) {
|
|
367
|
+
audit('rejected', { what: 'admin', op: data.op, by, reason: e.message })
|
|
368
|
+
return { ok: false, error: e.message }
|
|
369
|
+
}
|
|
370
|
+
return { ok: false, error: 'admin: invalid operation' }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return { handle, get nonceCount () { return seen.size } }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export default { createAdminDesk, ADMIN_OPS, ADMIN_OPS_WHILE_LOCKED, ADMIN_NONCE_TTL_MS }
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* FABRICAR EL SOBRE DE UNA VARIABLE — del lado de quien escribe.
|
|
380
|
+
*
|
|
381
|
+
* La bóveda ya no ve el valor (dueño, 2026-09-01): recibe el sobre hecho, comprueba lo que
|
|
382
|
+
* puede sin la llave —quién lo firma y a quién envuelve— y lo guarda. Esto es lo que hay que
|
|
383
|
+
* mandarle, y vive aquí para que no lo rearme cada consola por su cuenta.
|
|
384
|
+
*
|
|
385
|
+
* El orden importa y no es casual:
|
|
386
|
+
* 1. se pide a la BÓVEDA a quién hay que envolver (`var.recipients`). La lista sale del
|
|
387
|
+
* acta y la sabe ella; tener dos sitios respondiendo a eso es cómo se deja fuera a
|
|
388
|
+
* alguien sin que nadie se entere.
|
|
389
|
+
* 2. CEK nueva y el valor cifrado con ella;
|
|
390
|
+
* 3. esa CEK envuelta a la PÚBLICA de cada destinatario, más la de recuperación —sin la
|
|
391
|
+
* cual el cajón nacería ilegible para siempre;
|
|
392
|
+
* 4. y la firma del AUTOR sobre el cajón, la clave y el sobre.
|
|
393
|
+
*
|
|
394
|
+
* QUIÉN FIRMA SE PASA COMO FUNCIÓN, no como llave. En el navegador la privada del aparato
|
|
395
|
+
* vive dentro del iframe de identidad y NO SALE de ahí —esa es media garantía del diseño—,
|
|
396
|
+
* así que la consola no puede firmar aquí: pide la firma y ya. En Node, donde la llave sí
|
|
397
|
+
* está a mano, se envuelve `signWithDevice` en dos líneas. Un solo camino para los dos.
|
|
398
|
+
*
|
|
399
|
+
* @param {{recipients:{recoveryPub:string, members:Array<{pub:string,encPub:string}>},
|
|
400
|
+
* owner:string, key:string, value:string,
|
|
401
|
+
* author:{publickey:string, sign:(body:any)=>Promise<{signature:string}>}}} opts
|
|
402
|
+
*/
|
|
403
|
+
export async function buildSealedVar ({ recipients, owner, key, value, author } = {}) {
|
|
404
|
+
const { makeContentKey, encryptWithCek, wrapForMember } = await import('@dotrino/identity/content')
|
|
405
|
+
if (!recipients?.recoveryPub) throw new Error('buildSealedVar: ask the vault for the recipients first (var.recipients)')
|
|
406
|
+
if (typeof value !== 'string' || !value) throw new Error('buildSealedVar: the value must be a non-empty string')
|
|
407
|
+
if (typeof author?.sign !== 'function' || typeof author?.publickey !== 'string') {
|
|
408
|
+
throw new Error('buildSealedVar: author needs { publickey, sign(body) }')
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const cek = await makeContentKey()
|
|
412
|
+
const e = await encryptWithCek({ cek, gen: 0, plaintext: value })
|
|
413
|
+
// Envolver solo necesita PÚBLICAS: por eso esto se puede hacer en un navegador sin que
|
|
414
|
+
// ninguna llave privada ande suelta.
|
|
415
|
+
const wraps = { '#recovery': await wrapForMember({ cek, memberEncPub: recipients.recoveryPub }) }
|
|
416
|
+
for (const m of recipients.members || []) {
|
|
417
|
+
wraps[m.pub] = await wrapForMember({ cek, memberEncPub: m.encPub })
|
|
418
|
+
}
|
|
419
|
+
const ts = Date.now()
|
|
420
|
+
const { signature } = await author.sign(authorBody(owner, key, e, ts))
|
|
421
|
+
return { e, wraps, author: { pub: author.publickey, sig: signature, ts } }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* El `author` de `buildSealedVar` a partir de una llave de aparato de Node (la que trae
|
|
426
|
+
* `makeDeviceKey` o un `service-identity.json`). En el navegador no se usa: allí la firma
|
|
427
|
+
* la da el iframe.
|
|
428
|
+
*/
|
|
429
|
+
export function authorFromDeviceKey (device) {
|
|
430
|
+
return {
|
|
431
|
+
publickey: device.publickey,
|
|
432
|
+
sign: async (body) => {
|
|
433
|
+
const { signWithDevice } = await import('@dotrino/identity/capabilities')
|
|
434
|
+
return signWithDevice({ ...device, data: body })
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LA BÓVEDA EN EL NAVEGADOR: la pestaña (`vault.dotrino.com`, dentro del iframe de
|
|
3
|
+
* identidad) y la extensión del gestor de contraseñas corren el MISMO núcleo que el
|
|
4
|
+
* binario (`./core/vault.js`). Lo único distinto es lo que pone la plataforma:
|
|
5
|
+
*
|
|
6
|
+
* · el disco es IndexedDB cifrado (`./core/browserDisk.js`), uno por perfil;
|
|
7
|
+
* · la identidad es la que ya tiene abierta la pestaña o la extensión;
|
|
8
|
+
* · no hay `/proc` ni socket de archivo: lo que declara quien pide se queda como
|
|
9
|
+
* declarado, y los servicios de la misma máquina llegan por el proxio.
|
|
10
|
+
*
|
|
11
|
+
* Y lo que sí suma: la consola que corre en este mismo navegador administra esta bóveda
|
|
12
|
+
* por un canal local (`core.setSelfVault`), con el MISMO protocolo y las mismas
|
|
13
|
+
* comprobaciones que contra el binario.
|
|
14
|
+
*
|
|
15
|
+
* Lo que el binario tiene y el navegador no, por su contexto: actualizarse solo (la pestaña
|
|
16
|
+
* se sirve siempre al día y la extensión la actualiza su tienda), las llaves de hardware
|
|
17
|
+
* como puerta del perfil y el candado que cifra la maestra con la contraseña. Ver
|
|
18
|
+
* `docs/three-versions.md`.
|
|
19
|
+
*/
|
|
20
|
+
import { startVault } from './core/vault.js'
|
|
21
|
+
import { openBrowserDisk } from './core/browserDisk.js'
|
|
22
|
+
import { LIB_VERSION } from './core/libVersion.js'
|
|
23
|
+
import { IdentityFacade } from '@dotrino/identity/facade'
|
|
24
|
+
import { memberScopes } from '@dotrino/identity/acta'
|
|
25
|
+
import { deviceIdOf } from './enroll.js'
|
|
26
|
+
|
|
27
|
+
const DEFAULT_PROXY = 'wss://proxy.dotrino.com'
|
|
28
|
+
/** Cada cuánto se rehace el papel con el que la consola local administra esta bóveda. */
|
|
29
|
+
const SELF_CERT_EVERY_MS = 60_000
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {object} opts
|
|
33
|
+
* @param {object} opts.core el núcleo de identidad ya abierto (`createIdentityCore`)
|
|
34
|
+
* @param {string} [opts.proxyUrl]
|
|
35
|
+
* @param {Function} [opts.makeClient] quién crea el cliente del proxio. La extensión lo
|
|
36
|
+
* pasa siempre: un service worker no puede hacer `import()`.
|
|
37
|
+
* @param {Function} [opts.legacyLogins] los inicios de sesión que esta pestaña guardaba
|
|
38
|
+
* antes de correr el núcleo (`{ state, drop }` o `null`). Se traen UNA vez.
|
|
39
|
+
* @param {Function} [opts.onPendingChange] un aparato espera aprobación, o dejó de esperar
|
|
40
|
+
* @param {Function} [opts.onAdopted]
|
|
41
|
+
* @param {Function} [opts.log]
|
|
42
|
+
* @param {object} [opts.opaque] el servidor de OPAQUE, si el del paquete no se puede cargar aquí
|
|
43
|
+
* (la extensión: su CSP no deja WASM en la página de la bóveda).
|
|
44
|
+
* @param {boolean} [opts.servePasswords] `false` si las contraseñas las atiende quien monta
|
|
45
|
+
* la bóveda (la extensión, con su worker); decide con `passwordsNeedApproval`/`approvePasswordRequest`.
|
|
46
|
+
* @param {Function} [opts.openDisk] `(name) => disk`. Solo para las pruebas (Node no tiene
|
|
47
|
+
* IndexedDB); por defecto, `openBrowserDisk`.
|
|
48
|
+
*/
|
|
49
|
+
export async function startBrowserVault ({ core, proxyUrl = DEFAULT_PROXY, makeClient = null, legacyLogins = null, onPendingChange = () => {}, onAdopted = () => {}, log = console.log, openDisk = openBrowserDisk, opaque = null, servePasswords = true } = {}) {
|
|
50
|
+
if (!core?.handlers) throw Object.assign(new Error('startBrowserVault: the identity core is required'), { code: 'no-identity' })
|
|
51
|
+
const identity = new IdentityFacade(core)
|
|
52
|
+
if (!identity.me?.publickey) throw Object.assign(new Error('no identity: create or unlock your profile before using this browser as a vault'), { code: 'no-identity' })
|
|
53
|
+
const { id: pid } = await identity.currentProfile()
|
|
54
|
+
if (!pid) throw Object.assign(new Error('no profile: the vault belongs to an account'), { code: 'no-profile' })
|
|
55
|
+
|
|
56
|
+
const disk = await openDisk(`dotrino-vault.${pid}`)
|
|
57
|
+
|
|
58
|
+
// LOS INICIOS DE SESIÓN QUE YA HABÍA. Antes de correr el núcleo, la pestaña guardaba los
|
|
59
|
+
// usuarios y contraseñas de la cuenta en su propio almacén. Se traen una vez —si el disco
|
|
60
|
+
// ya tiene los suyos, mandan esos— y después se borra lo viejo: dos copias acaban diciendo
|
|
61
|
+
// cosas distintas.
|
|
62
|
+
if (legacyLogins && !disk.exists('logins.json')) {
|
|
63
|
+
const old = await legacyLogins()
|
|
64
|
+
if (old?.state) {
|
|
65
|
+
disk.write('logins.json', disk.atRest.encrypt(JSON.stringify(old.state)))
|
|
66
|
+
await disk.flush()
|
|
67
|
+
try { await old.drop?.() } catch (_) {}
|
|
68
|
+
log('[vault] password logins moved into the vault storage')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const avisar = () => { try { onPendingChange() } catch (_) {} }
|
|
73
|
+
const vault = await startVault({
|
|
74
|
+
platform: {
|
|
75
|
+
disk,
|
|
76
|
+
product: 'vault',
|
|
77
|
+
version: LIB_VERSION,
|
|
78
|
+
openIdentity: async () => identity,
|
|
79
|
+
makeClient,
|
|
80
|
+
noticeGroupMs: 3000,
|
|
81
|
+
...(opaque ? { opaque } : {}),
|
|
82
|
+
servePasswords
|
|
83
|
+
},
|
|
84
|
+
proxyUrl,
|
|
85
|
+
log,
|
|
86
|
+
isLocked: () => !!core.masterLocked,
|
|
87
|
+
// El navegador no tiene la contraseña del perfil con la que el binario cierra la copia
|
|
88
|
+
// de recuperación: se cierra con la llave de este almacén, y el núcleo lo dice.
|
|
89
|
+
hasPassword: () => false,
|
|
90
|
+
onEnrollChallenge: avisar,
|
|
91
|
+
onAdopted: (info) => { try { onAdopted(info) } catch (_) {} }
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// EL PAPEL CON EL QUE LA CONSOLA DE ESTE NAVEGADOR ADMINISTRA ESTA BÓVEDA: la llave del
|
|
95
|
+
// perfil firmándose a sí misma con lo que el acta le deja hoy. Se rehace cada minuto
|
|
96
|
+
// porque el acta cambia (un permiso que te quitas, un sellado nuevo).
|
|
97
|
+
const master = identity.me.publickey
|
|
98
|
+
const selfCert = async () => {
|
|
99
|
+
const acta = (await identity.profileActa().catch(() => null))?.acta || null
|
|
100
|
+
if (!acta) return null
|
|
101
|
+
const scope = memberScopes(acta, master)
|
|
102
|
+
if (!scope.length) return null
|
|
103
|
+
return (await identity.signDelegation(master, scope, { label: 'self' })).cert
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* EL DUEÑO EN SU MÁQUINA ADMINISTRA. En el binario eso es la TUI, que no necesita ningún
|
|
107
|
+
* permiso del acta; aquí la consola habla el protocolo de la consola remota, que exige
|
|
108
|
+
* `admin`. Así que la llave de la cuenta se lo da a sí misma, una vez y a la vista en el
|
|
109
|
+
* acta: es verdad que este navegador administra la cuenta. `aprueba` NO, a propósito: con
|
|
110
|
+
* él los servicios pasarían a esperar el visto bueno de una pestaña que puede no estar.
|
|
111
|
+
*/
|
|
112
|
+
const asegurarAdmin = async () => {
|
|
113
|
+
if (core.masterLocked) return
|
|
114
|
+
const acta = (await identity.profileActa().catch(() => null))?.acta || null
|
|
115
|
+
const yo = (acta?.members || []).find((m) => m.pub === master)
|
|
116
|
+
if (!yo || (yo.caps || []).includes('admin')) return
|
|
117
|
+
if (!(await identity.isMaster().catch(() => false))) return
|
|
118
|
+
await identity.setCaps(master, [...yo.caps, 'admin'])
|
|
119
|
+
log('[vault] this browser administers the account: admin added to its own key')
|
|
120
|
+
}
|
|
121
|
+
const open = (onReply) => vault.localChannel(onReply)
|
|
122
|
+
const presentar = async () => {
|
|
123
|
+
try {
|
|
124
|
+
await asegurarAdmin()
|
|
125
|
+
const cert = await selfCert()
|
|
126
|
+
core.setSelfVault?.(cert ? { cert, proxy: proxyUrl, open } : null)
|
|
127
|
+
} catch (e) { log('[vault] the console in this browser cannot administer this vault yet: ' + (e?.message || e)) }
|
|
128
|
+
}
|
|
129
|
+
await presentar()
|
|
130
|
+
const refresco = setInterval(presentar, SELF_CERT_EVERY_MS)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
...vault,
|
|
134
|
+
iss: master,
|
|
135
|
+
proxy: proxyUrl,
|
|
136
|
+
disk,
|
|
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 { avisar() } },
|
|
139
|
+
reject: (deviceId) => { try { return vault.rejectDevice(deviceId) } finally { avisar() } },
|
|
140
|
+
/**
|
|
141
|
+
* Las máquinas con papel de firma de esta cuenta (excluye los navegadores enrolados con
|
|
142
|
+
* etiqueta `cli`, que no atienden pedidos). Se queda el papel del acta más nueva.
|
|
143
|
+
*/
|
|
144
|
+
async listMachines () {
|
|
145
|
+
const { issued } = await identity.listDelegations()
|
|
146
|
+
const bySub = new Map()
|
|
147
|
+
for (const x of (issued || [])) {
|
|
148
|
+
if (!x.sub || x.revokedAt) continue
|
|
149
|
+
if (!Array.isArray(x.scope) || !x.scope.includes('vault:sign')) continue
|
|
150
|
+
if (!x.label || x.label === 'cli' || x.label === 'self') continue
|
|
151
|
+
if (!bySub.has(x.sub) || (x.seq || 0) > (bySub.get(x.sub).seq || 0)) bySub.set(x.sub, x)
|
|
152
|
+
}
|
|
153
|
+
return Promise.all([...bySub.values()].map(async (x) => ({ ...x, deviceId: await deviceIdOf(x.sub) })))
|
|
154
|
+
},
|
|
155
|
+
revokeDevice: (sub) => vault.revokeDevice({ sub }),
|
|
156
|
+
revoke: (nonce) => vault.revokeDevice(nonce),
|
|
157
|
+
async close () {
|
|
158
|
+
clearInterval(refresco)
|
|
159
|
+
try { core.setSelfVault?.(null) } catch (_) {}
|
|
160
|
+
vault.close()
|
|
161
|
+
try { await disk.flush() } catch (e) { log('[vault] some changes could not be saved: ' + (e?.message || e)) }
|
|
162
|
+
disk.close()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export default { startBrowserVault }
|