@dotrino/vaultd 0.138.0 → 0.139.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 +2 -2
- package/src/threadStore.js +46 -1
- package/src/vault.js +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.139.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Certificador personal de Dotrino: daemon headless que custodia la clave maestra y delega capacidades a tus dispositivos por el proxy. Tu CA propia.",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@dotrino/compat": "0.4.0",
|
|
25
|
-
"@dotrino/identity": "0.
|
|
25
|
+
"@dotrino/identity": "^0.107.0",
|
|
26
26
|
"@dotrino/opaque": "0.1.0",
|
|
27
27
|
"@dotrino/passmanager": "0.17.0",
|
|
28
28
|
"@dotrino/proxy-client": "0.27.1",
|
package/src/threadStore.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import path from 'node:path'
|
|
17
17
|
import * as core from '@dotrino/store/core'
|
|
18
|
+
import { resealStale } from '@dotrino/identity/content'
|
|
18
19
|
import { readJson, writeJson } from './paths.js'
|
|
19
20
|
import { atRestFor } from './atrest.js'
|
|
20
21
|
|
|
@@ -26,6 +27,11 @@ const MAX_PER_THREAD = core.MAX_PER_THREAD_LIMIT
|
|
|
26
27
|
// DATOS SENSIBLES (F4): topes para que un dispositivo con `vault:store` no pueda
|
|
27
28
|
// llenar el disco de la bóveda. Son generosos para el uso real (unas contraseñas,
|
|
28
29
|
// notas, un documento corto) y ridículos para un abuso.
|
|
30
|
+
// LO QUE SE VUELVE A CERRAR AL ROTAR NO SE BORRA: la versión anterior de cada sobre se
|
|
31
|
+
// guarda como respaldo AL MENOS UN AÑO (política del dueño, 2026-09-30). No se sirve por
|
|
32
|
+
// ningún método; solo está por si volver a cerrar sale mal.
|
|
33
|
+
export const RESEALED_KEEP_MS = 366 * 24 * 60 * 60 * 1000
|
|
34
|
+
|
|
29
35
|
const MAX_SECURE_ITEMS = 2000
|
|
30
36
|
const MAX_SECURE_BLOB = 64 * 1024 // por campo sellado (meta y valor)
|
|
31
37
|
|
|
@@ -203,7 +209,46 @@ export function openThreadStore (dir) {
|
|
|
203
209
|
return { threadCount: Object.keys(data.threads).length, threads, opensCount: Object.keys(data.opens).length, secureCount: Object.keys(data.secure).length }
|
|
204
210
|
}
|
|
205
211
|
}
|
|
206
|
-
|
|
212
|
+
/**
|
|
213
|
+
* VUELVE A CERRAR CON LA LLAVE VIGENTE todo sobre de la cuenta (`t: 'dotrino-cek'`) de
|
|
214
|
+
* una generación anterior que haya en lo guardado. Lo llama la BÓVEDA al abrirse y
|
|
215
|
+
* después de quitar un aparato —que es cuando la llave rota—; NO está en `methods`, así
|
|
216
|
+
* que ningún aparato puede dispararlo.
|
|
217
|
+
*
|
|
218
|
+
* Lo que sirve la bóveda va así siempre con la generación vigente: un aparato que entra
|
|
219
|
+
* después, que solo recibe esa, abre todo; y la vieja deja de abrir nada porque ya no se
|
|
220
|
+
* sirve nada cerrado con ella (dueño, 2026-09-30). Cada entrada que cambia estrena `ts`,
|
|
221
|
+
* para que los aparatos la bajen en su siguiente sincronización.
|
|
222
|
+
*
|
|
223
|
+
* La versión anterior NO se tira: queda en `resealed` con la fecha, por si algo sale mal
|
|
224
|
+
* (lo pidió el dueño: «podría retenerlos… pero no los envía»), y se guarda AL MENOS un
|
|
225
|
+
* año (`RESEALED_KEEP_MS`). No se sirve por ningún método.
|
|
226
|
+
*
|
|
227
|
+
* @param {{ gen: number, reseal: (env: object) => Promise<object> }} o
|
|
228
|
+
* @returns {Promise<{ changed: number, entries: number }>}
|
|
229
|
+
*/
|
|
230
|
+
async function resealStaleEnvelopes ({ gen, reseal }) {
|
|
231
|
+
let changed = 0; let entries = 0
|
|
232
|
+
const now = Date.now()
|
|
233
|
+
if (!data.resealed) data.resealed = []
|
|
234
|
+
// Solo sale lo que ya cumplió su año de respaldo.
|
|
235
|
+
const antes = data.resealed.length
|
|
236
|
+
data.resealed = data.resealed.filter((x) => now - (x.at || 0) < RESEALED_KEEP_MS)
|
|
237
|
+
const podados = antes - data.resealed.length
|
|
238
|
+
for (const [key, arr] of Object.entries(data.threads)) {
|
|
239
|
+
for (let i = 0; i < arr.length; i++) {
|
|
240
|
+
const r = await resealStale(arr[i], { gen, reseal })
|
|
241
|
+
if (!r.changed) continue
|
|
242
|
+
data.resealed.push({ threadKey: key, at: now, gen, before: arr[i] })
|
|
243
|
+
arr[i] = { ...r.value, ts: Math.max(now, (arr[i].ts || 0) + 1) }
|
|
244
|
+
changed += r.changed; entries++
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (entries || podados) save()
|
|
248
|
+
return { changed, entries }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return { methods, raw: () => data, resealStaleEnvelopes }
|
|
207
252
|
}
|
|
208
253
|
|
|
209
254
|
/**
|
package/src/vault.js
CHANGED
|
@@ -116,6 +116,26 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
116
116
|
|
|
117
117
|
const store = openStore(dir)
|
|
118
118
|
const threads = openThreadStore(dir)
|
|
119
|
+
/**
|
|
120
|
+
* LO QUE SIRVE LA BÓVEDA VA CON LA LLAVE VIGENTE (dueño, 2026-09-30). Tras una rotación
|
|
121
|
+
* de la llave de la cuenta, los sobres (`t: 'dotrino-cek'`) de generaciones anteriores
|
|
122
|
+
* que haya en el almacén se vuelven a cerrar con la vigente. Se llama al abrir y después
|
|
123
|
+
* de quitar un aparato. Si la bóveda no tiene la llave de contenido, no hay nada que
|
|
124
|
+
* hacer y se dice; si un sobre no abre, se para y se dice — no se deja a medias.
|
|
125
|
+
*/
|
|
126
|
+
async function resealStaleThreads (why) {
|
|
127
|
+
const mine = await identity.contentKey?.()
|
|
128
|
+
if (!mine) { log(`[vault] ${why}: no content key on this vault, envelopes not resealed`); return null }
|
|
129
|
+
const r = await threads.resealStaleEnvelopes({
|
|
130
|
+
gen: mine.gen,
|
|
131
|
+
reseal: async (env) => identity.sealContent(await identity.openContent(env))
|
|
132
|
+
})
|
|
133
|
+
if (r.changed) {
|
|
134
|
+
audit('resealed', { envelopes: r.changed, entries: r.entries, gen: mine.gen })
|
|
135
|
+
log(`[vault] ${why}: resealed ${r.changed} envelope(s) in ${r.entries} entr${r.entries === 1 ? 'y' : 'ies'} with content key generation ${mine.gen}`)
|
|
136
|
+
}
|
|
137
|
+
return r
|
|
138
|
+
}
|
|
119
139
|
const approvals = createApprovals()
|
|
120
140
|
/**
|
|
121
141
|
* LO QUE YA SE APROBÓ: comando + carpeta, por una hora desde el último uso.
|
|
@@ -3845,6 +3865,10 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
3845
3865
|
}
|
|
3846
3866
|
} catch (e) { log(`[vault] unlock: could not remove expired devices: ${e.message}`) }
|
|
3847
3867
|
}
|
|
3868
|
+
// Y DESPUÉS, LOS SOBRES A LA LLAVE VIGENTE: la poda de arriba pudo rotarla.
|
|
3869
|
+
if (r?.locked === false) {
|
|
3870
|
+
try { await resealStaleThreads('unlock') } catch (e) { log(`[vault] unlock: could not reseal old envelopes: ${e.message}`) }
|
|
3871
|
+
}
|
|
3848
3872
|
return { locked: r?.locked !== false }
|
|
3849
3873
|
},
|
|
3850
3874
|
// Forzar un empujón: lo usa `replica push` de la CLI y el smoke, para no depender
|
|
@@ -4077,6 +4101,8 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
4077
4101
|
const r = await desk.revokeDevice(sub)
|
|
4078
4102
|
audit('revoke-device', { device: await deviceIdOf(sub).catch(() => null), certs: r?.nonces?.length ?? null })
|
|
4079
4103
|
await refreshWraps('revoked')
|
|
4104
|
+
// Quitar un aparato rota la llave de la cuenta: lo guardado pasa a la nueva.
|
|
4105
|
+
try { await resealStaleThreads('revoke') } catch (e) { log(`[vault] revoke: could not reseal old envelopes: ${e.message}`) }
|
|
4080
4106
|
await notifyMembers('revoked', { deviceId: await deviceIdOf(sub).catch(() => null), by: 'pc' })
|
|
4081
4107
|
return r
|
|
4082
4108
|
},
|