@dotrino/identity 0.114.1 → 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.
Files changed (81) hide show
  1. package/package.json +7 -1
  2. package/src/index.js +9 -1
  3. package/src/node.js +4 -180
  4. package/vault/acta.js +5 -1
  5. package/vault/core.js +95 -15
  6. package/vault/facade.js +200 -0
  7. package/vault/index.html +10 -0
  8. package/vault/machine.js +95 -0
  9. package/vault/remote.js +54 -9
  10. package/vault/vault.js +29 -58
  11. package/vault/vendor/compat/VERSION.txt +3 -0
  12. package/vault/vendor/compat/advisory.js +141 -0
  13. package/vault/vendor/compat/index.js +188 -0
  14. package/vault/vendor/compat/ranges.js +93 -0
  15. package/vault/vendor/opaque/VERSION.txt +1 -1
  16. package/vault/vendor/passmanager/VERSION.txt +3 -0
  17. package/vault/vendor/passmanager/crypto.js +167 -0
  18. package/vault/vendor/passmanager/fields.js +183 -0
  19. package/vault/vendor/passmanager/generate.js +74 -0
  20. package/vault/vendor/passmanager/import.js +156 -0
  21. package/vault/vendor/passmanager/index.js +27 -0
  22. package/vault/vendor/passmanager/match.js +137 -0
  23. package/vault/vendor/passmanager/model.js +332 -0
  24. package/vault/vendor/passmanager/pubkey.js +33 -0
  25. package/vault/vendor/passmanager/sealed/convert.js +72 -0
  26. package/vault/vendor/passmanager/sealed/device.js +211 -0
  27. package/vault/vendor/passmanager/sealed/entry.js +209 -0
  28. package/vault/vendor/passmanager/sealed/index.js +20 -0
  29. package/vault/vendor/passmanager/sealed/keys.js +120 -0
  30. package/vault/vendor/passmanager/sealed/recovery.js +136 -0
  31. package/vault/vendor/passmanager/sealed/store.js +508 -0
  32. package/vault/vendor/passmanager/session-cache.js +60 -0
  33. package/vault/vendor/passmanager/totp.js +111 -0
  34. package/vault/vendor/passmanager/transport/protocol.js +83 -0
  35. package/vault/vendor/passmanager/transport/proxy.js +112 -0
  36. package/vault/vendor/passmanager/transport/sealed.js +74 -0
  37. package/vault/vendor/passmanager/vault/approval.js +63 -0
  38. package/vault/vendor/passmanager/vault/errors.js +34 -0
  39. package/vault/vendor/passmanager/vault/guard.js +114 -0
  40. package/vault/vendor/passmanager/vault/interface.js +34 -0
  41. package/vault/vendor/passmanager/vault/local.js +213 -0
  42. package/vault/vendor/passmanager/vault/remote.js +85 -0
  43. package/vault/vendor/passmanager/vault/responder.js +192 -0
  44. package/vault/vendor/passmanager/vault/sealed-local.js +199 -0
  45. package/vault/vendor/passmanager/vault/sealed-responder.js +159 -0
  46. package/vault/vendor/passmanager/vault/sealed.js +362 -0
  47. package/vault/vendor/passmanager/webauthn.js +221 -0
  48. package/vault/vendor/proxy-client/VERSION.txt +1 -1
  49. package/vault/vendor/proxy-client/client.js +36 -0
  50. package/vault/vendor/roadmap/VERSION.txt +3 -0
  51. package/vault/vendor/roadmap/index.js +108 -0
  52. package/vault/vendor/roadmap/manifests.js +797 -0
  53. package/vault/vendor/store/VERSION.txt +3 -0
  54. package/vault/vendor/store/core.js +309 -0
  55. package/vault/vendor/vault/VERSION.txt +4 -1
  56. package/vault/vendor/vault/admin.js +437 -0
  57. package/vault/vendor/vault/browser.js +167 -0
  58. package/vault/vendor/vault/core/approvals.js +204 -0
  59. package/vault/vendor/vault/core/blocked.js +68 -0
  60. package/vault/vendor/vault/core/browserDisk.js +109 -0
  61. package/vault/vendor/vault/core/commKey.js +68 -0
  62. package/vault/vendor/vault/core/disk.js +65 -0
  63. package/vault/vendor/vault/core/libVersion.js +8 -0
  64. package/vault/vendor/vault/core/machines.js +66 -0
  65. package/vault/vendor/vault/core/notices.js +71 -0
  66. package/vault/vendor/vault/core/notifiable.js +73 -0
  67. package/vault/vendor/vault/core/passwords.js +125 -0
  68. package/vault/vendor/vault/core/sealKey.js +77 -0
  69. package/vault/vendor/vault/core/sealer.js +172 -0
  70. package/vault/vendor/vault/core/sealers.js +159 -0
  71. package/vault/vendor/vault/core/secretsStore.js +1326 -0
  72. package/vault/vendor/vault/core/sha256.js +69 -0
  73. package/vault/vendor/vault/core/store.js +68 -0
  74. package/vault/vendor/vault/core/subacta.js +101 -0
  75. package/vault/vendor/vault/core/threadStore.js +273 -0
  76. package/vault/vendor/vault/core/transport.js +270 -0
  77. package/vault/vendor/vault/core/vault.js +4414 -0
  78. package/vault/vendor/vault/enroll.js +8 -4
  79. package/vault/vendor/vault/procContext.js +97 -0
  80. package/vault/vendor/vault/revocation.js +67 -0
  81. package/vault/vendor/vault/sealed.js +84 -0
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Transporte headless del vault: el cliente OFICIAL `@dotrino/proxy-client`
3
+ * corriendo en Node (mismo patrón que `dotrino-bots/src/core/transport.js`).
4
+ * Hace el `identify` — liga el token efímero de la conexión a una pubkey estable,
5
+ * habilitando el direccionamiento por pubkey (`sendByPubkey`) y la cola offline
6
+ * de 24 h del proxy.
7
+ *
8
+ * QUIÉN FIRMA ESE `identify`, que es lo que cambió: **la llave de comunicación**
9
+ * (`commKey.js`), no la maestra. La maestra sella el acta y reenvuelve sobres, y
10
+ * nada más; si además tuviera que firmar aquí no podría vivir bajo llave, porque
11
+ * esto se firma en cada conexión Y en cada reconexión — una bóveda cerrada no
12
+ * existiría en la red.
13
+ *
14
+ * Lo que ata el token al PERFIL entonces ya no es un certificado sino el ACTA: el
15
+ * proxio comprueba que quien habla sea miembro (`verifyActaMembership`) y ata el
16
+ * token también al `profileId`. Por eso se manda el acta siempre.
17
+ *
18
+ * IMPORTANTE: `me.publickey` puede ser null en el primer arranque (solo se puebla
19
+ * al fijar un nickname), así que la pubkey maestra se obtiene de forma robusta
20
+ * con un `signData` de cortesía si hace falta. El daemon (vault.js) además puebla
21
+ * `me` antes de conectar.
22
+ */
23
+
24
+ const DEFAULT_PROXY = 'wss://proxy.dotrino.com'
25
+
26
+ export async function masterPubkeyOf (identity) {
27
+ return identity.me?.publickey || (await identity.signData({ op: 'whoami', ts: Date.now() })).publickey
28
+ }
29
+
30
+ /**
31
+ * Conecta el transporte y lo identifica con la maestra del vault.
32
+ * @param {Object} opts
33
+ * @param {import('@dotrino/identity/facade').IdentityFacade} opts.identity
34
+ * @param {Function} [opts.makeClient] quién crea el cliente del proxio. Sin él, el del
35
+ * ecosistema; el navegador lo pasa siempre (un service worker no puede hacer `import()`).
36
+ * @param {string} [opts.url] URL del proxy (default wss://proxy.dotrino.com).
37
+ * @returns {Promise<{ client, token:string, identify():Promise<void> }>}
38
+ */
39
+ export async function createTransport ({ identity, url = DEFAULT_PROXY, commKey = null, log = () => {}, makeClient = null, announce = null, recheckMs = 60_000, patienceMs = 5 * 60_000, reannounceMs = 10 * 60_000 }) {
40
+ // Import dinámico DESPUÉS de instalar los globals que el paquete usa.
41
+ // `WebSocketProxyClient` (la clase) y NO el helper `getWebSocketProxyClient`:
42
+ // ese es un SINGLETON de proceso, y con multi-perfil el vault necesita una
43
+ // conexión POR PERFIL (cada maestra se identifica con su propia pubkey ante el
44
+ // proxy). Con el singleton, el segundo perfil reusaba el cliente del primero y
45
+ // su `identify` pisaba al anterior. Sigue siendo el cliente oficial del paquete.
46
+ // `makeClient` es la costura de las PRUEBAS, y existe por una razón concreta: comprobar
47
+ // que un `identify` rechazado no tumba el perfil y se reintenta solo. Sin ella habría que
48
+ // levantar un proxio de verdad para probar el caso que ya costó una tarde. En producción
49
+ // no se pasa nunca y esto es el cliente oficial de siempre.
50
+ const { WebSocketProxyClient } = makeClient ? {} : await import('@dotrino/proxy-client')
51
+
52
+ /**
53
+ * EL CANAL DIRECTO, ENCENDIDO Y CON LA PUERTA CERRADA.
54
+ *
55
+ * Encendido porque es el escalón 2 de la regla del transporte (`CLAUDE.md`) y en Node ya
56
+ * hay con qué (`werift`, que el pilar carga solo). Si no lo hubiera, negociar falla y se
57
+ * sigue por el proxio — el escalón 4 siempre está.
58
+ *
59
+ * Y con la puerta cerrada desde el primer día, que es lo que hace defendible encenderlo:
60
+ * **solo se le negocia a quien está en el acta.** Aceptar una señal arranca DTLS, ICE y
61
+ * SCTP, o sea código que parsea red no confiable, dentro del proceso que guarda la
62
+ * maestra. Quien decide si eso corre no puede ser el que llama a la puerta.
63
+ *
64
+ * Reconexión prácticamente ilimitada: un daemon de larga duración no debe rendirse tras
65
+ * unos intentos.
66
+ */
67
+ // La puerta tiene que decidir AL INSTANTE (la señal llega y o se atiende o no), y pedir
68
+ // el acta es asíncrono. Así que se guarda una copia y se refresca donde ya se refresca
69
+ // todo: en `identify`, que es lo que corre cada vez que el acta cambia o se reconecta.
70
+ let actaVista = null
71
+ // ¿Está esta bóveda ATADA al proxio ahora mismo? Conectado no es identificado: el socket
72
+ // puede estar perfectamente abierto y el sobre rechazado, y entonces nadie la alcanza por
73
+ // su pubkey. Se expone para que `status` lo enseñe en vez de dejarlo a un log del arranque.
74
+ let identificado = false
75
+ const enElActa = (token) => {
76
+ // SIN ACTA NO SE NEGOCIA. No se sabe quién es nadie, y en la duda no se arranca a
77
+ // parsear red no confiable. Se sigue por el proxio, que es el escalón que siempre está.
78
+ if (!actaVista) return false
79
+ return (actaVista.members || []).some((m) => m?.pub === token)
80
+ }
81
+ const client = makeClient ? makeClient({ url, acceptDirectFrom: enElActa }) : new WebSocketProxyClient({
82
+ url, enableWebRTC: true, acceptDirectFrom: enElActa, autoReconnect: true,
83
+ maxReconnectAttempts: 100000, reconnectDelay: 4000
84
+ })
85
+
86
+ await client.connect()
87
+
88
+ const identify = async () => {
89
+ identificado = false
90
+ if (!client.token) return
91
+ const record = (await identity.profileActa?.().catch(() => null))?.acta || null
92
+ actaVista = record // la copia que mira la puerta del canal directo
93
+ const comm = commKey?.pub?.() || null
94
+ // ¿Nos nombra el acta? Con `cn` o sin él, lo que el proxio mira es la pertenencia.
95
+ const esMiembro = comm && (record?.members || []).some((m) => m?.pub === comm)
96
+
97
+ if (comm && esMiembro) {
98
+ // El sobre lo arma el pilar (`identifyAs`), que le pone el destinatario.
99
+ try {
100
+ await client.identifyAs({ publickey: comm, sign: (d) => commKey.sign(d), acta: record })
101
+ identificado = true
102
+ // SE DICE QUE SE IDENTIFICÓ, y con qué llave. Este camino era MUDO: al pasar de la
103
+ // maestra a la llave de comunicación, el único rastro de «estoy alcanzable» que
104
+ // quedaba en el log era el del repliegue. Así que cuando la consola decía «no
105
+ // contesta», no había forma de saber desde aquí si la bóveda estaba atada al proxio
106
+ // o no. Una línea por reconexión es barata; no tenerla cuesta una hora de sondeos.
107
+ log(`[vault] identified on the proxy with the communication key · record #${record?.seq ?? '?'}`)
108
+ return
109
+ } catch (e) {
110
+ // SOLO «esta llave no firma» cae al camino de abajo, y se distingue por el `code`.
111
+ // Un fallo de red no es eso: tragárselo aquí mandaría a la maestra a firmar por un
112
+ // problema pasajero, que es justo lo que la llave de comunicación vino a evitar.
113
+ if (e?.code !== 'no-signature') throw e
114
+ }
115
+ }
116
+
117
+ // REPLIEGUE, y solo para migrar: una bóveda que todavía no ha metido su llave de
118
+ // comunicación en el acta se identifica con la maestra, como siempre. Exige el perfil
119
+ // ABIERTO —la maestra bajo llave no firma— y por eso se dice en voz alta: mientras se
120
+ // esté aquí, cerrar el perfil deja la bóveda sin voz.
121
+ //
122
+ // QUEDARSE SIN VOZ NO ES NO ARRANCAR, y aquí eran lo mismo. Un perfil con la maestra
123
+ // sellada y sin llave de comunicación todavía llegaba hasta este `signData`, que tira
124
+ // `vault locked`; el error subía por `createTransport` hasta `startVault` y **el perfil
125
+ // entero se quedaba sin abrir**. Al dueño le pasó el 2026-09-02: su perfil principal no
126
+ // arrancaba, `unlock` lo marcaba abierto igual y cada petición contestaba «profile is
127
+ // not open», con la TUI vacía y sin un error a la vista.
128
+ //
129
+ // Se dice y se sigue. Sin red se puede vivir hasta que alguien teclee la frase; sin
130
+ // bóveda, no.
131
+ try {
132
+ const publickey = await masterPubkeyOf(identity)
133
+ if (!publickey) return
134
+ log('[vault] identifying with the master key: this vault is not in its own record yet (open the profile once to fix it)')
135
+ await client.identifyAs({ publickey, sign: (d) => identity.signData(d), acta: record })
136
+ identificado = true
137
+ } catch (e) {
138
+ log(`[vault] cannot identify on the proxy yet: ${e.message} — the profile still opens; unlock it once and it gets its own communication key`)
139
+ }
140
+ }
141
+ /**
142
+ * IDENTIFICARSE PUEDE FALLAR, Y ESO NO PUEDE TUMBAR EL PERFIL — NI QUEDARSE ASÍ.
143
+ *
144
+ * Esto era `await identify()` a secas, así que cualquier «no» del proxio subía por
145
+ * `createTransport` hasta `startVault` y el perfil **no se abría en absoluto**: no es que
146
+ * se quedara sin voz, es que cada petición contestaba `profile is not open` y en el
147
+ * `status` el perfil salía sin huella. Y no se reintentaba nunca: `identify` solo vuelve a
148
+ * correr con el evento `token`, o sea al RECONECTAR, y aquí el socket estaba perfectamente
149
+ * conectado — lo que el proxio rechazó fue el sobre.
150
+ *
151
+ * El caso real (2026-09-10): esta máquina arranca con el reloj 35 minutos atrasado (el
152
+ * `systemd-timesyncd` no contactó el servidor de hora hasta media hora después del
153
+ * arranque), el proxio rechaza el `identify` con «ts fuera de la ventana ±5min» y la
154
+ * bóveda se quedaba fuera de la red DESDE EL ARRANQUE HASTA QUE ALGUIEN LA REINICIABA.
155
+ * Con ella fuera no hay a quién timbrar: el teléfono nunca recibía un pedido de
156
+ * aprobación, y desde fuera eso se ve igual que «no hay pedidos».
157
+ *
158
+ * Reintentar con espera creciente lo arregla solo en cuanto el reloj se corrige, y cada
159
+ * intento se dice en voz alta: una bóveda inalcanzable en silencio es el fallo que ya
160
+ * costó un día entero (`CLAUDE.md`, el apagón del 1-2 de septiembre).
161
+ */
162
+ /**
163
+ * EL ANUNCIO DE LA CUENTA (`temporary-access.md` §3.2).
164
+ *
165
+ * Quien entra con usuario y contraseña no tiene ninguna llave todavía: no sabe la pubkey
166
+ * de esta bóveda y no puede escribirle. Lo único que trae es el código de la dirección
167
+ * (`nombre@AB12-CD34-EF56`), y con él lista este canal para encontrar a quien atiende esa
168
+ * cuenta — esta bóveda, o cualquiera de sus réplicas.
169
+ *
170
+ * Va PEGADO a `identify` y no en el arranque, porque la pertenencia a un canal es del
171
+ * token, y el token cambia en cada reconexión: anunciarse una sola vez deja a la bóveda
172
+ * fuera del canal en cuanto se cae el socket, y eso se ve desde fuera como «esa cuenta no
173
+ * existe». Si el anuncio falla, la bóveda sigue sirviendo a todo lo demás y se dice en voz
174
+ * alta: lo que se pierde es que la encuentren por su dirección, no que deje de funcionar.
175
+ */
176
+ let announcedAt = 0
177
+ const announceAccount = async () => {
178
+ if (!announce || !identificado) return
179
+ announcedAt = Date.now()
180
+ try { await client.publish(announce) } catch (e) {
181
+ log(`[vault] could not announce this account on the proxy (${e.message}) — a borrowed machine will not find this vault by its address`)
182
+ }
183
+ }
184
+
185
+ let reintento = null
186
+ let fallos = 0
187
+ const identifyWithRetry = async () => {
188
+ if (reintento) { clearTimeout(reintento); reintento = null }
189
+ try {
190
+ await identify()
191
+ await announceAccount()
192
+ if (fallos) log(`[vault] identified on the proxy after ${fallos + 1} attempt(s)`)
193
+ fallos = 0
194
+ } catch (e) {
195
+ fallos++
196
+ const espera = Math.min(60000, 5000 * fallos)
197
+ log(`[vault] could not identify on the proxy (${e.message}) — retrying in ${Math.round(espera / 1000)}s; until then this vault is unreachable and cannot ring anyone`)
198
+ reintento = setTimeout(() => { reintento = null; identifyWithRetry() }, espera)
199
+ reintento.unref?.()
200
+ }
201
+ }
202
+ await identifyWithRetry()
203
+ // Re-identificar al reconectar (el token cambia).
204
+ client.on('token', () => { fallos = 0; identifyWithRetry() })
205
+
206
+ /**
207
+ * QUE NO SE PUEDA VOLVER A QUEDAR MUDA.
208
+ *
209
+ * El 16 de septiembre esta bóveda se cayó del proxio y no volvió en 36 HORAS, con el
210
+ * proceso vivo y sin una sola línea en el log: el cliente del transporte se rendía tras
211
+ * un reintento fallido (arreglado en `@dotrino/proxy-client` 0.23.0). Desde fuera eso se
212
+ * ve igual que «no pasa nada»: el teléfono no timbra y nadie sabe por qué.
213
+ *
214
+ * El arreglo del pilar quita ESA causa. Esto quita el SILENCIO, que es lo que costó las
215
+ * 36 horas — con cualquier causa, futura incluida:
216
+ *
217
+ * · se dice cuándo se cae y cuándo vuelve, una vez cada cosa (no una línea cada 4 s);
218
+ * · y si lleva 5 minutos fuera, se vuelve a marcar en vez de esperar sentada.
219
+ *
220
+ * `connect()` es seguro de repetir: si ya está conectado devuelve el token y no abre
221
+ * nada.
222
+ */
223
+ const RECHECK_MS = recheckMs
224
+ const PATIENCE_MS = patienceMs
225
+ const REANNOUNCE_MS = reannounceMs // la mitad de lo que el proxio guarda una entrada
226
+ let downSince = 0
227
+ let saidDown = false
228
+ client.on('disconnect', ({ code, reason } = {}) => {
229
+ // Sin socket no hay nadie atado al proxio: lo que se identificó era la conexión anterior.
230
+ identificado = false
231
+ if (!downSince) downSince = Date.now()
232
+ if (saidDown) return
233
+ saidDown = true
234
+ log(`[vault] off the proxy (${code || '?'}${reason ? ': ' + reason : ''}) — nobody can reach this vault while this lasts`)
235
+ })
236
+ // LOS INTENTOS SE VEN. El 2026-10-15 la bóveda pasó hora y media fuera y no quedó ni una
237
+ // línea de si el transporte estaba reintentando o no; en el proxio no llegó ninguno. El
238
+ // primero se dice, y luego uno de cada 15 (un minuto con el retardo de 4 s).
239
+ client.on('reconnecting', (attempt) => {
240
+ if (attempt === 1 || attempt % 15 === 0) log(`[vault] reconnecting to the proxy (attempt ${attempt})`)
241
+ })
242
+ client.on('reconnect_failed', (attempts) => {
243
+ log(`[vault] the transport gave up reconnecting after ${attempts} attempt(s) — this vault is unreachable until it is restarted`)
244
+ })
245
+ const watchdog = setInterval(async () => {
246
+ // CONECTADO DE VERDAD, no «tengo un token». El token es el de la última conexión y no se
247
+ // borra al caerse, así que mirarlo daba la bóveda por vuelta con el socket cerrado: el
248
+ // 2026-10-15 dijo «back on the proxy after 50s», no volvió a marcar nunca y pasó hora y
249
+ // media fuera, avisando cada 10 minutos de que no podía anunciarse.
250
+ if (identificado && client.isConnected && client.token) {
251
+ if (downSince) log(`[vault] back on the proxy after ${Math.round((Date.now() - downSince) / 1000)}s`)
252
+ downSince = 0; saidDown = false
253
+ // EL ANUNCIO CADUCA: el proxio borra una entrada de canal a los 20 minutos de publicada.
254
+ // Sin renovarlo, entrar con usuario y contraseña solo funcionaba los primeros 20 minutos
255
+ // tras identificarse, y desde fuera se veía como «no hay ninguna bóveda en esa dirección»
256
+ // con la bóveda encendida (2026-10-12, la demo de los revisores).
257
+ if (announce && Date.now() - announcedAt >= REANNOUNCE_MS) await announceAccount()
258
+ return
259
+ }
260
+ if (!downSince) downSince = Date.now()
261
+ const downMs = Date.now() - downSince
262
+ if (downMs < PATIENCE_MS) return
263
+ log(`[vault] ${Math.round(downMs / 60000)} min off the proxy — dialling again`)
264
+ try { await client.connect() } catch (e) { log(`[vault] could not reconnect: ${e.message}`) }
265
+ await identifyWithRetry()
266
+ }, RECHECK_MS)
267
+ watchdog.unref?.()
268
+
269
+ return { client, token: client.token, identify: identifyWithRetry, isIdentified: () => identificado }
270
+ }