@dotrino/vaultd 0.114.0 → 0.115.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 -1
- package/src/approvals.js +7 -0
- package/src/threadStore.js +60 -27
- package/src/vault.js +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.115.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": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"@dotrino/passmanager": "^0.10.1",
|
|
27
27
|
"@dotrino/proxy-client": "0.18.2",
|
|
28
28
|
"@dotrino/roadmap": "0.2.0",
|
|
29
|
+
"@dotrino/store": "0.11.0",
|
|
29
30
|
"ws": "^8.18.0"
|
|
30
31
|
},
|
|
31
32
|
"license": "MIT",
|
package/src/approvals.js
CHANGED
|
@@ -23,6 +23,13 @@
|
|
|
23
23
|
* otra máquina por el proxio, donde mandarlo en claro filtraría las rutas del dueño— no
|
|
24
24
|
* tiene huella con la que emparejarse, y entonces se pide aprobación cada vez. No se
|
|
25
25
|
* inventa una huella «vacía» que valdría para todo: eso sería exactamente un repliegue.
|
|
26
|
+
*
|
|
27
|
+
* Y PARA EL GESTOR DE CONTRASEÑAS ESO ES LO QUE SE QUIERE, no una carencia que arreglar
|
|
28
|
+
* (dueño, 2026-09-11: «para el gestor de contraseñas es perfecto»). La extensión es un
|
|
29
|
+
* navegador hablando por el proxio, no un proceso con línea de comandos, así que nunca va a
|
|
30
|
+
* traer huella — y así cada acceso a una credencial vuelve a pasar por tu mano, que es la
|
|
31
|
+
* protección por la que esto existe. No se le busque un sustituto del comando para que
|
|
32
|
+
* entre en la ventana.
|
|
26
33
|
*/
|
|
27
34
|
import { commandFingerprint } from '../lib/src/proc.js'
|
|
28
35
|
|
package/src/threadStore.js
CHANGED
|
@@ -14,10 +14,14 @@
|
|
|
14
14
|
* que el ecosistema promete que no queda en claro en ningún disco.
|
|
15
15
|
*/
|
|
16
16
|
import path from 'node:path'
|
|
17
|
+
import * as core from '@dotrino/store/core'
|
|
17
18
|
import { readJson, writeJson } from './paths.js'
|
|
18
19
|
import { atRestFor } from './atrest.js'
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
// El MISMO tope que acepta la página del almacén. Era 1000: una app que guarda más en un
|
|
22
|
+
// hilo (los compradores de facturero, por ejemplo) perdía aquí lo más viejo sin decir nada,
|
|
23
|
+
// y el navegador lo volvía a subir en cada sincronización.
|
|
24
|
+
const MAX_PER_THREAD = core.MAX_PER_THREAD_LIMIT
|
|
21
25
|
|
|
22
26
|
// DATOS SENSIBLES (F4): topes para que un dispositivo con `vault:store` no pueda
|
|
23
27
|
// llenar el disco de la bóveda. Son generosos para el uso real (unas contraseñas,
|
|
@@ -33,24 +37,31 @@ export function openThreadStore (dir) {
|
|
|
33
37
|
if (!data.threads) data.threads = {}
|
|
34
38
|
if (!data.opens) data.opens = {}
|
|
35
39
|
if (!data.secure) data.secure = {}
|
|
40
|
+
// Lápidas de lo borrado (`@dotrino/store/core`): sin ellas un borrado vuelve desde el
|
|
41
|
+
// primer aparato que todavía tenga la entrada.
|
|
42
|
+
if (!data.tombs) data.tombs = {}
|
|
43
|
+
core.pruneTombs(data.tombs, Date.now())
|
|
36
44
|
const save = () => writeJson(file, data, atRest)
|
|
37
45
|
save() // reescribe al abrir: cifra lo que venía en claro
|
|
38
|
-
const
|
|
46
|
+
const budget = (n) => Math.min(Math.max(Number(n) || core.PAGE_BYTES, 1024), core.PAGE_BYTES)
|
|
47
|
+
const needKeys = (keys) => {
|
|
48
|
+
if (!Array.isArray(keys) || !keys.every((k) => typeof k === 'string')) throw new Error('keys must be a list of thread keys')
|
|
49
|
+
return keys
|
|
50
|
+
}
|
|
51
|
+
const needMap = (value, what) => {
|
|
52
|
+
if (value == null) return {}
|
|
53
|
+
if (typeof value !== 'object' || Array.isArray(value)) throw new Error(`${what} must be an object`)
|
|
54
|
+
return value
|
|
55
|
+
}
|
|
39
56
|
|
|
40
57
|
const methods = {
|
|
41
58
|
appendMessage ({ threadKey, entry }) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!entry.id) entry.id = crypto.randomUUID()
|
|
45
|
-
if (!entry.ts) entry.ts = Date.now()
|
|
46
|
-
const arr = data.threads[threadKey] || (data.threads[threadKey] = [])
|
|
47
|
-
const i = arr.findIndex((e) => e.id === entry.id)
|
|
48
|
-
if (i >= 0) arr[i] = { ...arr[i], ...entry }; else arr.push(entry)
|
|
49
|
-
trim(arr); save(); return entry
|
|
59
|
+
core.writeEntry(data.threads, data.tombs, threadKey, entry, { max: MAX_PER_THREAD, now: Date.now(), newId: () => crypto.randomUUID() })
|
|
60
|
+
save(); return entry
|
|
50
61
|
},
|
|
51
62
|
listThread ({ threadKey, limit, before }) {
|
|
52
|
-
if (!threadKey) return []
|
|
53
|
-
let arr = data.threads[threadKey]
|
|
63
|
+
if (!core.validKey(threadKey) || !Object.hasOwn(data.threads, threadKey)) return []
|
|
64
|
+
let arr = data.threads[threadKey]
|
|
54
65
|
if (typeof before === 'number') arr = arr.filter((e) => (e.ts || 0) < before)
|
|
55
66
|
if (typeof limit === 'number' && limit > 0) arr = arr.slice(-limit)
|
|
56
67
|
return arr
|
|
@@ -62,14 +73,14 @@ export function openThreadStore (dir) {
|
|
|
62
73
|
return out
|
|
63
74
|
},
|
|
64
75
|
removeThread ({ threadKey }) {
|
|
65
|
-
const removed = data.threads
|
|
66
|
-
|
|
76
|
+
const removed = core.removeWholeThread(data.threads, data.tombs, threadKey, Date.now())
|
|
77
|
+
if (removed) save()
|
|
78
|
+
return { removed }
|
|
67
79
|
},
|
|
68
80
|
removeMessage ({ threadKey, id }) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
save(); return { removed: before - (data.threads[threadKey]?.length || 0) }
|
|
81
|
+
const removed = core.removeEntry(data.threads, data.tombs, threadKey, id, Date.now())
|
|
82
|
+
if (removed) save()
|
|
83
|
+
return { removed }
|
|
73
84
|
},
|
|
74
85
|
recordOpen ({ appId }) {
|
|
75
86
|
if (!appId || typeof appId !== 'string') throw new Error('appId required')
|
|
@@ -79,17 +90,38 @@ export function openThreadStore (dir) {
|
|
|
79
90
|
},
|
|
80
91
|
getOpens () { return { ...data.opens } },
|
|
81
92
|
clearOpens () { data.opens = {}; save(); return { ok: true } },
|
|
93
|
+
/** Mezcla el contador de un aparato (el mayor de cada lado) y devuelve el resultado. */
|
|
94
|
+
mergeOpens ({ opens }) {
|
|
95
|
+
if (core.mergeOpens(data.opens, needMap(opens, 'opens'))) save()
|
|
96
|
+
return { ...data.opens }
|
|
97
|
+
},
|
|
82
98
|
exportThreads () { return { threads: data.threads } },
|
|
83
|
-
|
|
99
|
+
// ----- SINCRONIZAR POR PARTES (@dotrino/store ≥ 0.11) -----
|
|
100
|
+
// `exportThreads`/`importThreads` a secas mueven TODO en un mensaje, y el proxio corta
|
|
101
|
+
// los frames en 1 MB: con unos cientos de KB de datos la sincronización dejaba de caber.
|
|
102
|
+
// Con esto el aparato pregunta qué hilos difieren (una huella por hilo), baja su índice
|
|
103
|
+
// (id y ts) y pide solo las entradas que le faltan, todo por páginas con tope de bytes.
|
|
104
|
+
// Las reglas —huella, mezcla, lápidas— son las de `@dotrino/store/core`, las mismas que
|
|
105
|
+
// aplica el navegador: si cada lado llevara las suyas, las huellas no coincidirían nunca.
|
|
106
|
+
getThreadDigests ({ keys } = {}) {
|
|
107
|
+
return core.digestsOf(data.threads, keys == null ? undefined : needKeys(keys))
|
|
108
|
+
},
|
|
109
|
+
getThreadIndexes ({ keys, cursor, maxBytes }) {
|
|
110
|
+
return core.indexPage(data.threads, data.tombs, needKeys(keys), cursor || null, budget(maxBytes))
|
|
111
|
+
},
|
|
112
|
+
getEntries ({ refs, maxBytes }) {
|
|
113
|
+
return core.entriesPage(data.threads, needMap(refs, 'refs'), budget(maxBytes))
|
|
114
|
+
},
|
|
115
|
+
importThreads ({ threads, tombs, mode = 'merge' }) {
|
|
84
116
|
if (!threads || typeof threads !== 'object') throw new Error('threads required')
|
|
85
117
|
if (mode === 'replace') { data.threads = threads; save(); return { mode, count: Object.keys(threads).length } }
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
118
|
+
if (mode !== 'merge' && mode !== 'upsert') throw new Error(`unknown import mode: ${mode}`)
|
|
119
|
+
const now = Date.now()
|
|
120
|
+
const buried = core.applyTombs(data.threads, data.tombs, needMap(tombs, 'tombs'), now)
|
|
121
|
+
const merged = core.mergeEntries(data.threads, data.tombs, threads, { mode, max: MAX_PER_THREAD })
|
|
122
|
+
core.pruneTombs(data.tombs, now)
|
|
123
|
+
if (buried.tombsChanged || buried.changed.size || merged.size) save()
|
|
124
|
+
return { mode, count: Object.keys(data.threads).length, changed: [...new Set([...buried.changed, ...merged])] }
|
|
93
125
|
},
|
|
94
126
|
// ----- PERFIL del usuario (me): el vault es la copia AUTORITATIVA -----
|
|
95
127
|
// Cada dispositivo emparejado lo empuja al editarlo y lo jala al arrancar →
|
|
@@ -182,7 +214,8 @@ export function openThreadStore (dir) {
|
|
|
182
214
|
* dispositivo al que solo le diste «leer» no lee tus contraseñas.
|
|
183
215
|
*/
|
|
184
216
|
export const STORE_READ_METHODS = new Set([
|
|
185
|
-
'listThread', 'listThreadKeys', 'getThreadSummaries', 'getOpens', 'exportThreads', 'getStats', 'profileGet'
|
|
217
|
+
'listThread', 'listThreadKeys', 'getThreadSummaries', 'getOpens', 'exportThreads', 'getStats', 'profileGet',
|
|
218
|
+
'getThreadDigests', 'getThreadIndexes', 'getEntries'
|
|
186
219
|
])
|
|
187
220
|
|
|
188
221
|
/**
|
package/src/vault.js
CHANGED
|
@@ -936,7 +936,9 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
936
936
|
// `method: 'toString'` (o cualquier miembro heredado de Object) pasaba el filtro y
|
|
937
937
|
// se llamaba como si fuera del store.
|
|
938
938
|
if (!d || typeof d.method !== 'string' || !Object.hasOwn(threads.methods, d.method)) {
|
|
939
|
-
|
|
939
|
+
// Con `code`: un aparato más nuevo que esta bóveda pide métodos que aquí no existen, y
|
|
940
|
+
// tiene que poder decir «actualiza la bóveda» en vez de enseñar la frase.
|
|
941
|
+
return reply(from, { type: MSG.ERROR, error: 'store: invalid method', code: 'store-unknown-method' })
|
|
940
942
|
}
|
|
941
943
|
if (!isFresh(d)) return staleReply(from)
|
|
942
944
|
// EL PERFIL VA POR OTRO CAMINO, y por eso se atiende antes que nada
|