@dotrino/vaultd 0.138.0 → 0.139.1
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 +51 -1
- package/src/vault.js +27 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.139.1",
|
|
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.1",
|
|
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,51 @@ 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
|
+
* También MIGRA los sobres de la cuenta anteriores a la marca (`{ gen, iv, ct }` a secas,
|
|
224
|
+
* identity < 0.107): si abren con la llave de la cuenta se vuelven a cerrar marcados; si no,
|
|
225
|
+
* no eran de la cuenta y se cuentan en `skipped` sin tocarlos.
|
|
226
|
+
*
|
|
227
|
+
* La versión anterior NO se tira: queda en `resealed` con la fecha, por si algo sale mal
|
|
228
|
+
* (lo pidió el dueño: «podría retenerlos… pero no los envía»), y se guarda AL MENOS un
|
|
229
|
+
* año (`RESEALED_KEEP_MS`). No se sirve por ningún método.
|
|
230
|
+
*
|
|
231
|
+
* @param {{ gen: number, reseal: (env: object) => Promise<object> }} o
|
|
232
|
+
* @returns {Promise<{ changed: number, entries: number, skipped: number }>}
|
|
233
|
+
*/
|
|
234
|
+
async function resealStaleEnvelopes ({ gen, reseal }) {
|
|
235
|
+
let changed = 0; let entries = 0; let skipped = 0
|
|
236
|
+
const now = Date.now()
|
|
237
|
+
if (!data.resealed) data.resealed = []
|
|
238
|
+
// Solo sale lo que ya cumplió su año de respaldo.
|
|
239
|
+
const antes = data.resealed.length
|
|
240
|
+
data.resealed = data.resealed.filter((x) => now - (x.at || 0) < RESEALED_KEEP_MS)
|
|
241
|
+
const podados = antes - data.resealed.length
|
|
242
|
+
for (const [key, arr] of Object.entries(data.threads)) {
|
|
243
|
+
for (let i = 0; i < arr.length; i++) {
|
|
244
|
+
const r = await resealStale(arr[i], { gen, reseal })
|
|
245
|
+
skipped += r.skipped || 0
|
|
246
|
+
if (!r.changed) continue
|
|
247
|
+
data.resealed.push({ threadKey: key, at: now, gen, before: arr[i] })
|
|
248
|
+
arr[i] = { ...r.value, ts: Math.max(now, (arr[i].ts || 0) + 1) }
|
|
249
|
+
changed += r.changed; entries++
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (entries || podados) save()
|
|
253
|
+
return { changed, entries, skipped }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { methods, raw: () => data, resealStaleEnvelopes }
|
|
207
257
|
}
|
|
208
258
|
|
|
209
259
|
/**
|
package/src/vault.js
CHANGED
|
@@ -116,6 +116,27 @@ 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
|
+
if (r.skipped) log(`[vault] ${why}: ${r.skipped} unmarked envelope(s) that the account key does not open were left as they are`)
|
|
138
|
+
return r
|
|
139
|
+
}
|
|
119
140
|
const approvals = createApprovals()
|
|
120
141
|
/**
|
|
121
142
|
* LO QUE YA SE APROBÓ: comando + carpeta, por una hora desde el último uso.
|
|
@@ -3845,6 +3866,10 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
3845
3866
|
}
|
|
3846
3867
|
} catch (e) { log(`[vault] unlock: could not remove expired devices: ${e.message}`) }
|
|
3847
3868
|
}
|
|
3869
|
+
// Y DESPUÉS, LOS SOBRES A LA LLAVE VIGENTE: la poda de arriba pudo rotarla.
|
|
3870
|
+
if (r?.locked === false) {
|
|
3871
|
+
try { await resealStaleThreads('unlock') } catch (e) { log(`[vault] unlock: could not reseal old envelopes: ${e.message}`) }
|
|
3872
|
+
}
|
|
3848
3873
|
return { locked: r?.locked !== false }
|
|
3849
3874
|
},
|
|
3850
3875
|
// Forzar un empujón: lo usa `replica push` de la CLI y el smoke, para no depender
|
|
@@ -4077,6 +4102,8 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
4077
4102
|
const r = await desk.revokeDevice(sub)
|
|
4078
4103
|
audit('revoke-device', { device: await deviceIdOf(sub).catch(() => null), certs: r?.nonces?.length ?? null })
|
|
4079
4104
|
await refreshWraps('revoked')
|
|
4105
|
+
// Quitar un aparato rota la llave de la cuenta: lo guardado pasa a la nueva.
|
|
4106
|
+
try { await resealStaleThreads('revoke') } catch (e) { log(`[vault] revoke: could not reseal old envelopes: ${e.message}`) }
|
|
4080
4107
|
await notifyMembers('revoked', { deviceId: await deviceIdOf(sub).catch(() => null), by: 'pc' })
|
|
4081
4108
|
return r
|
|
4082
4109
|
},
|