@dotrino/vault 0.85.0 → 0.86.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/README.md +32 -32
- package/package.json +16 -4
- package/src/browser.js +167 -0
- package/src/core/approvals.js +204 -0
- package/src/core/blocked.js +68 -0
- package/src/core/browserDisk.js +109 -0
- package/src/core/commKey.js +68 -0
- package/src/core/disk.js +65 -0
- package/src/core/libVersion.js +8 -0
- package/src/core/machines.js +66 -0
- package/src/core/notices.js +71 -0
- package/src/core/notifiable.js +73 -0
- package/src/core/passwords.js +125 -0
- package/src/core/sealKey.js +77 -0
- package/src/core/sealer.js +172 -0
- package/src/core/sealers.js +159 -0
- package/src/core/secretsStore.js +1326 -0
- package/src/core/sha256.js +69 -0
- package/src/core/store.js +68 -0
- package/src/core/subacta.js +101 -0
- package/src/core/threadStore.js +273 -0
- package/src/core/transport.js +270 -0
- package/src/core/vault.js +4414 -0
- package/src/proc.js +2 -84
- package/src/procContext.js +97 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* El SELLADOR: lo único de los secretos que toca criptografía.
|
|
3
|
+
*
|
|
4
|
+
* `secretsStore.js` guarda la forma y las reglas y no sabe cifrar; le inyecta este
|
|
5
|
+
* puerto y le pide sobres. Así el store se prueba con un sellador falso y este archivo
|
|
6
|
+
* se lee entero de una sentada.
|
|
7
|
+
*
|
|
8
|
+
* QUÉ HACE. Cada cajón (un namespace, o el cajón propio de un aparato) tiene una CEK
|
|
9
|
+
* AES-256-GCM. Las variables privadas se cifran con ella. La CEK viaja de dos formas,
|
|
10
|
+
* y esa dualidad es todo el diseño:
|
|
11
|
+
*
|
|
12
|
+
* · **hacia los miembros** — envuelta a la llave de cifrado (`encPub`) de cada
|
|
13
|
+
* aparato que deba leer ese cajón, con ECDH efímero: quien abre solo necesita su
|
|
14
|
+
* propia privada. Es lo que va en el llavero del archivo y lo que viaja al agente.
|
|
15
|
+
* · **hacia quien administra** — en la COPIA MAESTRA, cifrada con la llave derivada
|
|
16
|
+
* de la contraseña del perfil. Es lo que permite sellar una variable nueva o
|
|
17
|
+
* re-envolver la CEK a un aparato que entra.
|
|
18
|
+
*
|
|
19
|
+
* Y hay una tercera forma que NO existe a propósito: **la CEK nunca se envuelve a la
|
|
20
|
+
* llave de esta bóveda**. Si se hiciera, el daemon podría abrir todo por su cuenta y
|
|
21
|
+
* esto no valdría nada — que es exactamente la situación de la que venimos.
|
|
22
|
+
*
|
|
23
|
+
* La contraseña no está en el disco, así que una copia del disco no abre las privadas.
|
|
24
|
+
* Lo que sí abre es todo lo demás; los límites honestos están en
|
|
25
|
+
* `docs/secretos-sellados.md` §3 y conviene leerlos antes de confiar de más.
|
|
26
|
+
*/
|
|
27
|
+
import {
|
|
28
|
+
makeContentKey, makeGeneration, encryptWithCek, decryptWithCek, wrapForMember, openWrap
|
|
29
|
+
} from '@dotrino/identity/content'
|
|
30
|
+
|
|
31
|
+
const b64 = (b) => { let s = ''; for (const x of new Uint8Array(b)) s += String.fromCharCode(x); return btoa(s) }
|
|
32
|
+
const un64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0))
|
|
33
|
+
/** AES-256-GCM con WebCrypto: el mismo en Node y en el navegador. */
|
|
34
|
+
const aesKey = (raw, uses) => crypto.subtle.importKey('raw', raw, { name: 'AES-GCM' }, false, uses)
|
|
35
|
+
|
|
36
|
+
/** La copia maestra va cifrada con AES-256-GCM bajo la llave derivada de la contraseña. */
|
|
37
|
+
const MASTER_V = 1
|
|
38
|
+
|
|
39
|
+
/** Se pidió abrir la copia maestra sin llave, o con la equivocada. */
|
|
40
|
+
export class WrongPassword extends Error {
|
|
41
|
+
constructor (msg = 'wrong password') {
|
|
42
|
+
super(msg)
|
|
43
|
+
this.code = 'WRONG_PASSWORD'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function assertKey (adminKey) {
|
|
48
|
+
if (!(adminKey instanceof Uint8Array) || adminKey.length !== 32) {
|
|
49
|
+
throw new WrongPassword('the derived key is missing or malformed (32 bytes expected)')
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Crea el sellador. No guarda estado ni recuerda la contraseña: cada operación recibe
|
|
55
|
+
* la llave derivada y la suelta. Que no se cachee es la mitad del valor de todo esto —
|
|
56
|
+
* una llave que se queda en memoria para siempre deja el disco protegido y la máquina
|
|
57
|
+
* igual de expuesta que antes.
|
|
58
|
+
*/
|
|
59
|
+
export function makeSealer () {
|
|
60
|
+
return {
|
|
61
|
+
/**
|
|
62
|
+
* Abre la copia maestra: `{ "ns:<ns>": cek, "dev:<pub>": cek }`. Un `blob` vacío
|
|
63
|
+
* devuelve un mapa vacío — es el primer arranque, no un error.
|
|
64
|
+
*/
|
|
65
|
+
async openMaster (blob, adminKey) {
|
|
66
|
+
assertKey(adminKey)
|
|
67
|
+
if (!blob) return {}
|
|
68
|
+
if (blob.v !== MASTER_V) throw new Error(`master copy: unknown format v${blob.v}`)
|
|
69
|
+
try {
|
|
70
|
+
// WebCrypto quiere la etiqueta pegada al final del cifrado; en disco van aparte.
|
|
71
|
+
const ct = un64(blob.ct); const tag = un64(blob.tag)
|
|
72
|
+
const both = new Uint8Array(ct.length + tag.length); both.set(ct); both.set(tag, ct.length)
|
|
73
|
+
const plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: un64(blob.iv) }, await aesKey(adminKey, ['decrypt']), both)
|
|
74
|
+
return JSON.parse(new TextDecoder().decode(plain))
|
|
75
|
+
} catch (_) {
|
|
76
|
+
// El tag de AES-GCM ES el verificador: si no cuadra, la contraseña no era.
|
|
77
|
+
// Por eso no hace falta guardar aparte un verificador de la contraseña, que
|
|
78
|
+
// sería un camino más barato para atacarla desde una copia del disco.
|
|
79
|
+
throw new WrongPassword()
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
async sealMaster (obj, adminKey) {
|
|
84
|
+
assertKey(adminKey)
|
|
85
|
+
const iv = crypto.getRandomValues(new Uint8Array(12))
|
|
86
|
+
const both = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, await aesKey(adminKey, ['encrypt']), new TextEncoder().encode(JSON.stringify(obj))))
|
|
87
|
+
return { v: MASTER_V, iv: b64(iv), ct: b64(both.subarray(0, both.length - 16)), tag: b64(both.subarray(both.length - 16)) }
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/** La CEK de un cajón, creándola si es la primera variable que entra ahí. */
|
|
91
|
+
async cekFor (master, owner) {
|
|
92
|
+
if (!master[owner]) master[owner] = await makeContentKey()
|
|
93
|
+
return master[owner]
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/** Una CEK NUEVA para el cajón, tirando la anterior. Rotar es esto. */
|
|
97
|
+
async newCek (master, owner) {
|
|
98
|
+
master[owner] = await makeContentKey()
|
|
99
|
+
return master[owner]
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/** Cifra un valor con la CEK del cajón. */
|
|
103
|
+
async encrypt (cek, plaintext, gen = 0) {
|
|
104
|
+
const { iv, ct } = await encryptWithCek({ cek, gen, plaintext })
|
|
105
|
+
return { iv, ct }
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/** Descifra con la CEK que corresponda al cajón. Solo lo usa quien administra. */
|
|
109
|
+
async decrypt (master, envelope, owner) {
|
|
110
|
+
const cek = master[owner]
|
|
111
|
+
if (!cek) throw new Error(`master copy: no key for drawer ${owner}`)
|
|
112
|
+
return decryptWithCek({ cek, envelope })
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Una CEK NUEVA, sin llavero de por medio. Es lo que usa cada escritura desde v5: la
|
|
117
|
+
* bóveda no puede reutilizar la de antes porque **no puede abrir ninguna envoltura
|
|
118
|
+
* para recuperarla** — sellar es una capacidad pública, abrir no. Una generación por
|
|
119
|
+
* escritura es la consecuencia, no un capricho (§8.7).
|
|
120
|
+
*/
|
|
121
|
+
async newKey () {
|
|
122
|
+
return makeContentKey()
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* El par de RECUPERACIÓN: su pública se guarda en claro —para poder envolverle sin
|
|
127
|
+
* saber nada— y su privada va sellada bajo la frase. Es la copia que se abre el día
|
|
128
|
+
* que no queda ningún aparato, y la que abre la TUI cuando el dueño pide ver un valor.
|
|
129
|
+
*/
|
|
130
|
+
async makeRecoveryPair () {
|
|
131
|
+
const pair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey', 'deriveBits'])
|
|
132
|
+
return {
|
|
133
|
+
pub: JSON.stringify(await crypto.subtle.exportKey('jwk', pair.publicKey)),
|
|
134
|
+
priv: await crypto.subtle.exportKey('jwk', pair.privateKey)
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/** Envuelve una CEK a UNA pública suelta (la de recuperación, que no es un miembro). */
|
|
139
|
+
async wrapForKey (cek, encPub) {
|
|
140
|
+
return wrapForMember({ cek, memberEncPub: encPub })
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Abre una envoltura con una privada ECDH **en JWK**. Lo hace quien PUEDE leer.
|
|
145
|
+
*
|
|
146
|
+
* La privada llega como JWK porque así se guarda (sellada bajo la frase, en un JSON),
|
|
147
|
+
* y `openWrap` quiere una `CryptoKey`: importarla es el puente, y va aquí porque este
|
|
148
|
+
* es el único archivo que toca criptografía.
|
|
149
|
+
*/
|
|
150
|
+
async openWrapWith (privJwk, wrap) {
|
|
151
|
+
const key = await crypto.subtle.importKey('jwk', privJwk, { name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits'])
|
|
152
|
+
return openWrap({ wrap, myEncPrivateKey: key })
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/** Descifra un sobre con la CEK ya abierta. */
|
|
156
|
+
async openValue (cek, envelope) {
|
|
157
|
+
return decryptWithCek({ cek, envelope })
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Envuelve la CEK a cada miembro. Los que no tengan llave de cifrado salen en
|
|
162
|
+
* `sinLlave` en vez de reventar: quien administra tiene que poder VERLO, porque el
|
|
163
|
+
* síntoma sería un servicio arrancando sin configuración y sin decir por qué.
|
|
164
|
+
*/
|
|
165
|
+
async wrapFor (cek, members) {
|
|
166
|
+
const { generation, sinLlave } = await makeGeneration({ members, cek })
|
|
167
|
+
return { wraps: generation.wraps, sinLlave }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export default { makeSealer, WrongPassword }
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sealers.js — depositar la cadena de selladores en el registro público.
|
|
3
|
+
*
|
|
4
|
+
* POR QUÉ ESTO EXISTE. Un acta que alguien guardó el año pasado no puede responder la única
|
|
5
|
+
* pregunta que importa al leer una firma vieja: *la llave que firmó esto, ¿sigue pudiendo
|
|
6
|
+
* sellar?* Si a una bóveda le quitaron el permiso, quien tenga el acta vieja no se entera
|
|
7
|
+
* nunca — y seguiría creyéndole. El registro es el sitio donde mirar.
|
|
8
|
+
*
|
|
9
|
+
* QUÉ SE MANDA: los ESLABONES (`sealerLinkOf`), nunca las actas. Y un eslabón no es un acta
|
|
10
|
+
* recortada: es un documento propio de ocho campos, firmado aparte y metido DENTRO del acta
|
|
11
|
+
* antes de firmarla (identity >= 0.72). Costó una vuelta descubrirlo — la primera cadena que
|
|
12
|
+
* llegó al registro se fue con los `label` y los `cn` de cada aparato, porque «un eslabón»
|
|
13
|
+
* era un acta entera. Un acta lleva el inventario de tu casa: no se publica, ni recortada.
|
|
14
|
+
*
|
|
15
|
+
* POR QUÉ SIN SELLAR, y no es un descuido (§4.1 pide sellar los mensajes dirigidos): lo que
|
|
16
|
+
* viaja aquí va a acabar en un repo PÚBLICO, que es su propósito. Es el mismo caso que los
|
|
17
|
+
* canales `publish`/`list` del proxio, exentos por la misma razón: no hay nada que ocultar.
|
|
18
|
+
* Y no filtra de más — quién lo manda ya lo sabe el proxio por el `identify`. Sellarlo,
|
|
19
|
+
* además, obligaría a conocer la llave de cifrado del testigo, y depositar tiene que poder
|
|
20
|
+
* hacerlo cualquiera sin pedirle nada a nadie.
|
|
21
|
+
*
|
|
22
|
+
* Va por `sendByPubkey` y no por `send`: al testigo se le conoce por su pubkey, y así lo
|
|
23
|
+
* depositado entra en la cola de 24 h del proxio si estuviera caído.
|
|
24
|
+
*
|
|
25
|
+
* CUÁNDO: al arrancar y después de cada acta. Publicar de más no cuesta nada (el registro
|
|
26
|
+
* contesta «ya estaba») y así no hay que llevar la cuenta de qué se depositó — un estado
|
|
27
|
+
* que se desincroniza en cuanto se restaura un disco.
|
|
28
|
+
*/
|
|
29
|
+
import * as Acta from '@dotrino/identity/acta'
|
|
30
|
+
import { verifyDelegation } from '@dotrino/identity/capabilities'
|
|
31
|
+
|
|
32
|
+
export const OP = 'sealers.publish'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* LA IDENTIDAD DEL TESTIGO DE DOTRINO — no su aparato (dueño, 2026-08-31: «¿no me puedes
|
|
36
|
+
* quemar un device en el código?»).
|
|
37
|
+
*
|
|
38
|
+
* Tenía razón y era el mismo fallo que ya costó caro con el `nodeId` del proxio: la pubkey
|
|
39
|
+
* de un APARATO muere en cuanto lo revocas o lo re-enrolas, y todo lo que la tuviera
|
|
40
|
+
* apuntada se queda hablando con una dirección muerta. Lo que no cambia nunca es la
|
|
41
|
+
* IDENTIDAD: el `profileId` es el nombre de la cuenta y es para siempre.
|
|
42
|
+
*
|
|
43
|
+
* Así que aquí va la identidad, y el aparato se DESCUBRE: el testigo se anuncia en el canal
|
|
44
|
+
* con su cert, y se acepta al que traiga uno emitido por esta identidad. Re-enrolarlo da un
|
|
45
|
+
* cert nuevo de la misma identidad y todo sigue solo; revocarlo lo saca del acta y su
|
|
46
|
+
* anuncio deja de valer.
|
|
47
|
+
*
|
|
48
|
+
* Y nadie configura nada: esto es una dirección pública, como `wss://proxy.dotrino.com`.
|
|
49
|
+
*/
|
|
50
|
+
export const DOTRINO_REGISTRY_ID = "{\"key_ops\":[\"verify\"],\"ext\":true,\"kty\":\"EC\",\"x\":\"McyhKieRw_Jcl3TY9EvLovL-ALzGoWqMOrfnFjT7XpA\",\"y\":\"V7tfpDWN37tPnhXnIFUlMaTepcTzzxPWTj_72PkpKPw\",\"crv\":\"P-256\"}"
|
|
51
|
+
|
|
52
|
+
/** El canal donde el testigo se anuncia (ver `@dotrino/sealers`). */
|
|
53
|
+
export const CANAL = 'dotrino.sealers'
|
|
54
|
+
|
|
55
|
+
/** Se le pregunta a cada anunciado quién es; contesta con su cert (ver `@dotrino/sealers`). */
|
|
56
|
+
export const WHOIS = 'sealers.whois'
|
|
57
|
+
|
|
58
|
+
export function startSealersPublisher ({ identity, client, log = console.log, registryId = DOTRINO_REGISTRY_ID, canal = CANAL } = {}) {
|
|
59
|
+
if (!registryId) return () => {}
|
|
60
|
+
|
|
61
|
+
/** Una respuesta a `sealers.whois`, o null si no contesta a tiempo. */
|
|
62
|
+
function preguntarQuienEs (token, timeoutMs = 8000) {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
const fin = (v) => { try { off() } catch (_) {} ; clearTimeout(t); resolve(v) }
|
|
65
|
+
const off = client.on('message', (_f, p) => { if (p?.op === WHOIS + '.result') fin(p) })
|
|
66
|
+
const t = setTimeout(() => fin(null), timeoutMs)
|
|
67
|
+
t.unref?.()
|
|
68
|
+
try { client.send(token, { op: WHOIS }) } catch (_) { fin(null) }
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* ¿A QUIÉN LE MANDO ESTO? Al que esté anunciado en el canal y demuestre ser de la
|
|
74
|
+
* IDENTIDAD que llevamos en el código.
|
|
75
|
+
*
|
|
76
|
+
* El canal dice DÓNDE mirar y nada más: `list` del proxio devuelve tokens sueltos — el
|
|
77
|
+
* `extraData` del `publish` no vuelve, comprobado contra el servidor. Así que a cada uno
|
|
78
|
+
* se le pregunta quién es (`sealers.whois`) y contesta con su cert. Que el cert venga del
|
|
79
|
+
* propio testigo y no del anuncio es además mejor: llega fresco.
|
|
80
|
+
*
|
|
81
|
+
* Sin comprobar el cert, cualquiera se anuncia ahí y se queda con los depósitos. No
|
|
82
|
+
* filtraría nada —el eslabón es público— pero se los tragaría EN SILENCIO, que es peor
|
|
83
|
+
* que un error.
|
|
84
|
+
*/
|
|
85
|
+
async function testigo () {
|
|
86
|
+
let tokens
|
|
87
|
+
try { tokens = await client.list(canal) } catch (_) { return null }
|
|
88
|
+
for (const token of tokens || []) {
|
|
89
|
+
const r = await preguntarQuienEs(token)
|
|
90
|
+
const cert = r?.cert
|
|
91
|
+
if (!cert?.sub || cert.iss !== registryId) continue
|
|
92
|
+
// AQUÍ SOMOS EXTRAÑOS: no tenemos el acta del registro, así que no podemos preguntarle
|
|
93
|
+
// quién sella. Lo que sí tenemos es su `profileId` quemado en el código —inmutable, a
|
|
94
|
+
// diferencia de la dirección de un aparato— y eso es lo que se ancla: aceptamos un
|
|
95
|
+
// papel firmado POR ESA LLAVE, para el acta que él diga.
|
|
96
|
+
//
|
|
97
|
+
// Es exactamente la misma fuerza que antes (`cert.iss === registryId` + firma válida),
|
|
98
|
+
// dicho con la forma nueva. Y tiene un límite que conviene decir: si el registro
|
|
99
|
+
// llegara a tener una SEGUNDA selladora, sus papeles no entrarían por aquí hasta que
|
|
100
|
+
// presente su cadena de eslabones — que es, con gracia, justo lo que este registro
|
|
101
|
+
// publica.
|
|
102
|
+
const v = await verifyDelegation({
|
|
103
|
+
cert, expectedSub: cert.sub, actaSeq: cert.seq, sealers: [registryId]
|
|
104
|
+
}).catch(() => ({ ok: false }))
|
|
105
|
+
if (v?.ok) return cert.sub
|
|
106
|
+
}
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let ultima = null
|
|
111
|
+
|
|
112
|
+
let avisadoDeOtroTestigo = false
|
|
113
|
+
|
|
114
|
+
async function publicar (motivo) {
|
|
115
|
+
let actas
|
|
116
|
+
try { actas = await identity.sealerChain?.() } catch (_) { return }
|
|
117
|
+
if (!Array.isArray(actas)) return
|
|
118
|
+
|
|
119
|
+
// SI LA CUENTA DECLARÓ SU PROPIO TESTIGO, no se deposita en el de Dotrino. No es que
|
|
120
|
+
// hacerlo filtre algo —el eslabón es público— sino que el `chainUrl` del génesis es la
|
|
121
|
+
// forma que tiene una cuenta de decir dónde vive su cadena, y decidir por ella sería
|
|
122
|
+
// exactamente lo que ese campo existe para impedir. Se dice una vez y se calla.
|
|
123
|
+
const genesis = actas[0]
|
|
124
|
+
if (genesis?.chainUrl && registryId === DOTRINO_REGISTRY_ID) {
|
|
125
|
+
if (!avisadoDeOtroTestigo) {
|
|
126
|
+
avisadoDeOtroTestigo = true
|
|
127
|
+
log(`[sealers] this account names its own registry (${genesis.chainUrl}): not depositing into Dotrino's`)
|
|
128
|
+
}
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
// De cada acta sale su eslabón, y las que no cambiaron el sellador no dan ninguno.
|
|
132
|
+
const chain = actas.map((a) => Acta.sealerLinkOf(a)).filter(Boolean)
|
|
133
|
+
// Una cuenta de una sola bóveda no tiene nada que refrescar: su conjunto de selladores
|
|
134
|
+
// no puede cambiar. El registro la rechazaría, así que ni se molesta en mandarla.
|
|
135
|
+
// Una cuenta anterior a identity 0.72 tampoco tiene eslabones, y es el mismo caso: no
|
|
136
|
+
// aparece en el registro hasta que selle un acta nueva.
|
|
137
|
+
if (chain.length < 2) return
|
|
138
|
+
|
|
139
|
+
const cabeza = chain[chain.length - 1]?.seq
|
|
140
|
+
if (cabeza === ultima) return
|
|
141
|
+
try {
|
|
142
|
+
const a = await testigo()
|
|
143
|
+
if (!a) { log('[sealers] no witness announced in ' + canal + ': the chain stays for the next time'); return }
|
|
144
|
+
await client.sendByPubkey(a, { op: OP, chain })
|
|
145
|
+
ultima = cabeza
|
|
146
|
+
log(`[sealers] chain up to #${cabeza} deposited (${motivo})`)
|
|
147
|
+
} catch (e) {
|
|
148
|
+
// Que no se pueda depositar no rompe nada de lo que la bóveda hace: el registro es
|
|
149
|
+
// una comodidad para terceros, no una dependencia. Se reintenta en la próxima acta.
|
|
150
|
+
log('[sealers] could not deposit the chain:', e.message)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
publicar('startup').catch(() => {})
|
|
155
|
+
const off = identity.onVault?.((e) => { if (e?.phase === 'acta') publicar(`record #${e.seq}`).catch(() => {}) })
|
|
156
|
+
return () => { try { off?.() } catch (_) {} }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export default { startSealersPublisher, OP, DOTRINO_REGISTRY_ID, CANAL }
|