@dotrino/vaultd 0.155.0 → 0.157.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 +51 -14
- package/bin/sea-entry.js +4 -0
- package/lib/src/browser.js +167 -0
- package/{src → lib/src/core}/approvals.js +1 -1
- package/{src → lib/src/core}/blocked.js +7 -10
- package/lib/src/core/browserDisk.js +109 -0
- package/{src → lib/src/core}/commKey.js +5 -8
- package/lib/src/core/disk.js +65 -0
- package/lib/src/core/libVersion.js +8 -0
- package/{src → lib/src/core}/machines.js +7 -10
- package/{src → lib/src/core}/notices.js +8 -12
- package/{src → lib/src/core}/notifiable.js +7 -10
- package/{src → lib/src/core}/sealKey.js +5 -8
- package/{src → lib/src/core}/sealer.js +12 -10
- package/{src → lib/src/core}/secretsStore.js +8 -11
- package/lib/src/core/sha256.js +69 -0
- package/{src → lib/src/core}/store.js +6 -9
- package/{src → lib/src/core}/subacta.js +5 -6
- package/{src → lib/src/core}/threadStore.js +5 -8
- package/{src → lib/src/core}/transport.js +4 -5
- package/lib/src/core/vault.js +4414 -0
- package/lib/src/proc.js +2 -84
- package/lib/src/procContext.js +97 -0
- package/package.json +3 -3
- package/src/ctl.js +41 -3
- package/src/daemon.js +1 -1
- package/src/disk.js +54 -0
- package/src/platform.js +139 -0
- package/src/service.js +247 -0
- package/src/tui/app.js +319 -127
- package/src/tui/gui.js +214 -0
- package/src/tui/i18n.js +25 -6
- package/src/tui/term.js +7 -2
- package/src/update.js +5 -1
- package/src/vault.js +10 -4455
- /package/{src → lib/src/core}/passwords.js +0 -0
- /package/{src → lib/src/core}/sealers.js +0 -0
|
@@ -12,9 +12,6 @@
|
|
|
12
12
|
* Cifrado en reposo como lo demás. Un archivo que existe y no se puede leer se LANZA: callar
|
|
13
13
|
* sería decir «no pasó nada» de algo que sí pasó.
|
|
14
14
|
*/
|
|
15
|
-
import fs from 'node:fs'
|
|
16
|
-
import path from 'node:path'
|
|
17
|
-
import crypto from 'node:crypto'
|
|
18
15
|
|
|
19
16
|
export const NOTICES_FILE = 'notices.json'
|
|
20
17
|
/** Cuánto se sigue contando un aviso. */
|
|
@@ -25,25 +22,24 @@ export const NOTICE_EVS = ['updated', 'update-needs-root']
|
|
|
25
22
|
const fail = (code, message) => Object.assign(new Error(message), { code })
|
|
26
23
|
|
|
27
24
|
/**
|
|
28
|
-
* @param {{
|
|
25
|
+
* @param {{ disk: object, now?: () => number }} opts
|
|
29
26
|
*/
|
|
30
|
-
export function openNotices ({
|
|
31
|
-
const file =
|
|
27
|
+
export function openNotices ({ disk, now = Date.now }) {
|
|
28
|
+
const file = NOTICES_FILE
|
|
29
|
+
const atRest = disk.atRest
|
|
32
30
|
let items = load()
|
|
33
31
|
|
|
34
32
|
function load () {
|
|
35
|
-
if (!
|
|
33
|
+
if (!disk.exists(file)) return []
|
|
36
34
|
try {
|
|
37
|
-
const d = JSON.parse(atRest.decrypt(
|
|
35
|
+
const d = JSON.parse(atRest.decrypt(disk.read(file)))
|
|
38
36
|
if (!d || !Array.isArray(d.items)) throw new Error('not a list')
|
|
39
37
|
return d.items.filter((n) => n && typeof n.id === 'string' && typeof n.ts === 'number')
|
|
40
38
|
} catch (e) { throw fail('notices-unreadable', `${NOTICES_FILE} cannot be read: ${e.message}`) }
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
function save () {
|
|
44
|
-
|
|
45
|
-
fs.writeFileSync(tmp, atRest.encrypt(JSON.stringify({ v: 1, items })), { mode: 0o600 })
|
|
46
|
-
fs.renameSync(tmp, file)
|
|
42
|
+
disk.write(file, atRest.encrypt(JSON.stringify({ v: 1, items })))
|
|
47
43
|
}
|
|
48
44
|
|
|
49
45
|
const alive = (n) => now() - n.ts < NOTICE_TTL_MS
|
|
@@ -60,7 +56,7 @@ export function openNotices ({ dir, atRest, now = Date.now }) {
|
|
|
60
56
|
if (typeof version !== 'string' || !version) throw fail('bad-notice', `${ev}: a version is required`)
|
|
61
57
|
if (typeof product !== 'string' || !product) throw fail('bad-notice', `${ev}: a product is required`)
|
|
62
58
|
const n = {
|
|
63
|
-
id: crypto.
|
|
59
|
+
id: [...crypto.getRandomValues(new Uint8Array(8))].map((b) => b.toString(16).padStart(2, '0')).join(''), ev, product, version, from: typeof from === 'string' ? from : null,
|
|
64
60
|
...(typeof deviceId === 'string' && deviceId ? { deviceId, label: typeof label === 'string' ? label : '' } : {}),
|
|
65
61
|
ts: now()
|
|
66
62
|
}
|
|
@@ -22,8 +22,6 @@
|
|
|
22
22
|
* Cifrado en reposo como lo demás. Un archivo que existe y no se puede leer NO es «nadie
|
|
23
23
|
* puede recibir avisos»: se lanza.
|
|
24
24
|
*/
|
|
25
|
-
import fs from 'node:fs'
|
|
26
|
-
import path from 'node:path'
|
|
27
25
|
|
|
28
26
|
export const NOTIFIABLE_FILE = 'notifiable.json'
|
|
29
27
|
/** Cuánto vale un «puedo recibir avisos» sin renovarse. */
|
|
@@ -32,19 +30,20 @@ export const NOTIFIABLE_TTL_MS = 30 * 24 * 60 * 60 * 1000
|
|
|
32
30
|
const fail = (code, message) => Object.assign(new Error(message), { code })
|
|
33
31
|
|
|
34
32
|
/**
|
|
35
|
-
* @param {{
|
|
33
|
+
* @param {{ disk: object, now?: () => number }} opts
|
|
36
34
|
*/
|
|
37
|
-
export function openNotifiable ({
|
|
38
|
-
const file =
|
|
35
|
+
export function openNotifiable ({ disk, now = Date.now }) {
|
|
36
|
+
const file = NOTIFIABLE_FILE
|
|
37
|
+
const atRest = disk.atRest
|
|
39
38
|
/** pub → ms de la última vez que dijo que sí */
|
|
40
39
|
const items = load()
|
|
41
40
|
|
|
42
41
|
function load () {
|
|
43
42
|
let raw
|
|
44
|
-
try { raw =
|
|
45
|
-
if (e.code === 'ENOENT') return new Map()
|
|
43
|
+
try { raw = disk.read(file) } catch (e) {
|
|
46
44
|
throw fail('notifiable-unreadable', `cannot read ${NOTIFIABLE_FILE}: ${e.message}`)
|
|
47
45
|
}
|
|
46
|
+
if (raw == null) return new Map()
|
|
48
47
|
let d
|
|
49
48
|
try { d = JSON.parse(atRest.decrypt(raw)) } catch (e) { throw fail('notifiable-unreadable', `${NOTIFIABLE_FILE} cannot be opened: ${e.message}`) }
|
|
50
49
|
if (d?.v !== 1 || !d.items || typeof d.items !== 'object') throw fail('notifiable-unreadable', `${NOTIFIABLE_FILE} has an unknown shape`)
|
|
@@ -52,9 +51,7 @@ export function openNotifiable ({ dir, atRest, now = Date.now }) {
|
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
function save () {
|
|
55
|
-
|
|
56
|
-
fs.writeFileSync(tmp, atRest.encrypt(JSON.stringify({ v: 1, items: Object.fromEntries(items) })), { mode: 0o600 })
|
|
57
|
-
fs.renameSync(tmp, file)
|
|
54
|
+
disk.write(file, atRest.encrypt(JSON.stringify({ v: 1, items: Object.fromEntries(items) })))
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
return {
|
|
@@ -17,9 +17,7 @@
|
|
|
17
17
|
* No guarda ninguna correspondencia con `seq`: quién mandaba y cuándo lo dice el acta
|
|
18
18
|
* (`sealKeys` con su tramo). Aquí solo están las privadas, indexadas por su pública.
|
|
19
19
|
*/
|
|
20
|
-
import
|
|
21
|
-
import { readJson, writeJson } from './paths.js'
|
|
22
|
-
import { atRestFor } from './atrest.js'
|
|
20
|
+
import { readJson, writeJson } from './disk.js'
|
|
23
21
|
import { signWithDevice } from '@dotrino/identity/capabilities'
|
|
24
22
|
|
|
25
23
|
/**
|
|
@@ -30,13 +28,12 @@ import { signWithDevice } from '@dotrino/identity/capabilities'
|
|
|
30
28
|
const MAX_KEYS = 8
|
|
31
29
|
|
|
32
30
|
/** Abre (o estrena) el llavero de sellado del perfil. */
|
|
33
|
-
export function openSealKeys (
|
|
34
|
-
const file =
|
|
35
|
-
const
|
|
36
|
-
const data = readJson(file, null, atRest) || { v: 1, keys: [] }
|
|
31
|
+
export function openSealKeys (disk) {
|
|
32
|
+
const file = 'sealkeys.json'
|
|
33
|
+
const data = readJson(disk, file, null) || { v: 1, keys: [] }
|
|
37
34
|
if (!Array.isArray(data.keys)) data.keys = []
|
|
38
35
|
|
|
39
|
-
const save = () => writeJson(file, data
|
|
36
|
+
const save = () => writeJson(disk, file, data)
|
|
40
37
|
const find = (pub) => data.keys.find((k) => k.pub === pub) || null
|
|
41
38
|
|
|
42
39
|
return {
|
|
@@ -24,13 +24,14 @@
|
|
|
24
24
|
* Lo que sí abre es todo lo demás; los límites honestos están en
|
|
25
25
|
* `docs/secretos-sellados.md` §3 y conviene leerlos antes de confiar de más.
|
|
26
26
|
*/
|
|
27
|
-
import crypto from 'node:crypto'
|
|
28
27
|
import {
|
|
29
28
|
makeContentKey, makeGeneration, encryptWithCek, decryptWithCek, wrapForMember, openWrap
|
|
30
29
|
} from '@dotrino/identity/content'
|
|
31
30
|
|
|
32
|
-
const b64 = (b) =>
|
|
33
|
-
const un64 = (s) =>
|
|
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)
|
|
34
35
|
|
|
35
36
|
/** La copia maestra va cifrada con AES-256-GCM bajo la llave derivada de la contraseña. */
|
|
36
37
|
const MASTER_V = 1
|
|
@@ -66,9 +67,11 @@ export function makeSealer () {
|
|
|
66
67
|
if (!blob) return {}
|
|
67
68
|
if (blob.v !== MASTER_V) throw new Error(`master copy: unknown format v${blob.v}`)
|
|
68
69
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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))
|
|
72
75
|
} catch (_) {
|
|
73
76
|
// El tag de AES-GCM ES el verificador: si no cuadra, la contraseña no era.
|
|
74
77
|
// Por eso no hace falta guardar aparte un verificador de la contraseña, que
|
|
@@ -79,10 +82,9 @@ export function makeSealer () {
|
|
|
79
82
|
|
|
80
83
|
async sealMaster (obj, adminKey) {
|
|
81
84
|
assertKey(adminKey)
|
|
82
|
-
const iv = crypto.
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
return { v: MASTER_V, iv: b64(iv), ct: b64(ct), tag: b64(c.getAuthTag()) }
|
|
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)) }
|
|
86
88
|
},
|
|
87
89
|
|
|
88
90
|
/** La CEK de un cajón, creándola si es la primera variable que entra ahí. */
|
|
@@ -53,10 +53,8 @@
|
|
|
53
53
|
* pide sobres. Así se prueba con un sellador falso y determinista, y la forma del
|
|
54
54
|
* archivo se razona sin mirar una sola llave.
|
|
55
55
|
*/
|
|
56
|
-
import
|
|
57
|
-
import {
|
|
58
|
-
import { atRestFor } from './atrest.js'
|
|
59
|
-
import { isValidSecretsNs } from './protocol.js'
|
|
56
|
+
import { readJson, writeJson } from './disk.js'
|
|
57
|
+
import { isValidSecretsNs } from '../protocol.js'
|
|
60
58
|
|
|
61
59
|
const SCHEMA_VERSION = 5
|
|
62
60
|
/** v4: sellado, pero con la copia maestra bajo la frase. Se lee y se sirve; no se escribe. */
|
|
@@ -168,10 +166,9 @@ function migrateBag (bag) {
|
|
|
168
166
|
* `defaultKey`: con qué sellar la privada de recuperación cuando el perfil NO tiene
|
|
169
167
|
* contraseña. Vive aquí, en un solo sitio, para que dé igual por qué puerta se entre.
|
|
170
168
|
*/
|
|
171
|
-
export function openSecretsStore (
|
|
172
|
-
const file =
|
|
173
|
-
|
|
174
|
-
let data = readJson(file, null, atRest)
|
|
169
|
+
export function openSecretsStore (disk, { sealer = null, recipients = null, signer = null, defaultKey = null } = {}) {
|
|
170
|
+
const file = 'secrets.json'
|
|
171
|
+
let data = readJson(disk, file, null)
|
|
175
172
|
|
|
176
173
|
// v1 → v2: el cajón por scope se conserva y estrena el `dev`.
|
|
177
174
|
// v2 → v3: los valores dejan de ser strings sueltos y pasan a llevar su visibilidad.
|
|
@@ -196,13 +193,13 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
|
|
|
196
193
|
const isLegacy = () => data.schemaVersion === LEGACY_VERSION
|
|
197
194
|
const needsMigration = () => data.schemaVersion === SEALED_MASTER_VERSION
|
|
198
195
|
|
|
199
|
-
writeJson(file, data
|
|
196
|
+
writeJson(disk, file, data) // reescribe al abrir: cifra lo que venía en claro
|
|
200
197
|
|
|
201
198
|
// Un grupo de escrituras se guarda UNA vez (ver `batch`). Fuera de un grupo, `held`
|
|
202
199
|
// vale 0 y cada cambio va al disco en el acto, como siempre.
|
|
203
200
|
let held = 0
|
|
204
201
|
let pending = false
|
|
205
|
-
const flush = () => writeJson(file, data
|
|
202
|
+
const flush = () => writeJson(disk, file, data)
|
|
206
203
|
const save = () => { if (held) { pending = true; return } ; flush() }
|
|
207
204
|
|
|
208
205
|
const assertNs = (ns) => {
|
|
@@ -1313,7 +1310,7 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
|
|
|
1313
1310
|
|
|
1314
1311
|
// Copia de lo anterior antes de pisarlo. Con nodos en producción, deshacer tiene
|
|
1315
1312
|
// que ser un `mv`, no una restauración. La borra el operador a mano.
|
|
1316
|
-
writeJson(`${file}.v${desde}.bak`, data
|
|
1313
|
+
writeJson(disk, `${file}.v${desde}.bak`, data)
|
|
1317
1314
|
data = next
|
|
1318
1315
|
flush()
|
|
1319
1316
|
return { migrated: true, from: desde, sinLlave }
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHA-256 SÍNCRONO, en JavaScript puro.
|
|
3
|
+
*
|
|
4
|
+
* El navegador solo trae `crypto.subtle.digest`, que es asíncrono, y la bitácora de la
|
|
5
|
+
* bóveda encadena cada línea con el hash de la anterior en el mismo paso en que la escribe:
|
|
6
|
+
* volverlo asíncrono dejaría dos entradas apuntando a la misma anterior. Así el binario y
|
|
7
|
+
* el navegador encadenan exactamente igual, y lo comprueba la prueba contra `node:crypto`.
|
|
8
|
+
*/
|
|
9
|
+
const K = new Uint32Array([
|
|
10
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
11
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
12
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
13
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
14
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
15
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
16
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
17
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
/** @param {Uint8Array} data */
|
|
21
|
+
export function sha256Bytes (data) {
|
|
22
|
+
const h = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19])
|
|
23
|
+
const len = data.length
|
|
24
|
+
const total = ((len + 9 + 63) >> 6) << 6
|
|
25
|
+
const buf = new Uint8Array(total)
|
|
26
|
+
buf.set(data)
|
|
27
|
+
buf[len] = 0x80
|
|
28
|
+
const bits = len * 8
|
|
29
|
+
const view = new DataView(buf.buffer)
|
|
30
|
+
view.setUint32(total - 8, Math.floor(bits / 0x100000000))
|
|
31
|
+
view.setUint32(total - 4, bits >>> 0)
|
|
32
|
+
const w = new Uint32Array(64)
|
|
33
|
+
for (let off = 0; off < total; off += 64) {
|
|
34
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4)
|
|
35
|
+
for (let i = 16; i < 64; i++) {
|
|
36
|
+
const a = w[i - 15], b = w[i - 2]
|
|
37
|
+
const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3)
|
|
38
|
+
const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10)
|
|
39
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0
|
|
40
|
+
}
|
|
41
|
+
let [a, b, c, d, e, f, g, hh] = h
|
|
42
|
+
for (let i = 0; i < 64; i++) {
|
|
43
|
+
const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7))
|
|
44
|
+
const ch = (e & f) ^ (~e & g)
|
|
45
|
+
const t1 = (hh + S1 + ch + K[i] + w[i]) >>> 0
|
|
46
|
+
const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10))
|
|
47
|
+
const maj = (a & b) ^ (a & c) ^ (b & c)
|
|
48
|
+
const t2 = (S0 + maj) >>> 0
|
|
49
|
+
hh = g; g = f; f = e; e = (d + t1) >>> 0
|
|
50
|
+
d = c; c = b; b = a; a = (t1 + t2) >>> 0
|
|
51
|
+
}
|
|
52
|
+
h[0] = (h[0] + a) >>> 0; h[1] = (h[1] + b) >>> 0; h[2] = (h[2] + c) >>> 0; h[3] = (h[3] + d) >>> 0
|
|
53
|
+
h[4] = (h[4] + e) >>> 0; h[5] = (h[5] + f) >>> 0; h[6] = (h[6] + g) >>> 0; h[7] = (h[7] + hh) >>> 0
|
|
54
|
+
}
|
|
55
|
+
const out = new Uint8Array(32)
|
|
56
|
+
const ov = new DataView(out.buffer)
|
|
57
|
+
for (let i = 0; i < 8; i++) ov.setUint32(i * 4, h[i])
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** El hash de un texto (UTF-8), en hexadecimal. Lo mismo que `createHash('sha256').update(text, 'utf8').digest('hex')`. */
|
|
62
|
+
export function sha256Hex (text) {
|
|
63
|
+
const bytes = typeof text === 'string' ? new TextEncoder().encode(text) : text
|
|
64
|
+
let s = ''
|
|
65
|
+
for (const b of sha256Bytes(bytes)) s += b.toString(16).padStart(2, '0')
|
|
66
|
+
return s
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default { sha256Bytes, sha256Hex }
|
|
@@ -6,9 +6,7 @@
|
|
|
6
6
|
* `@dotrino/identity` dentro del mismo dir (keypair, contactos, delegaciones,
|
|
7
7
|
* revocaciones). Aquí vive solo lo del usuario: el árbol y los settings.
|
|
8
8
|
*/
|
|
9
|
-
import
|
|
10
|
-
import { readJson, writeJson } from './paths.js'
|
|
11
|
-
import { atRestFor } from './atrest.js'
|
|
9
|
+
import { readJson, writeJson } from './disk.js'
|
|
12
10
|
|
|
13
11
|
const SCHEMA_VERSION = 1
|
|
14
12
|
|
|
@@ -28,17 +26,16 @@ function findNode (node, id) {
|
|
|
28
26
|
return null
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
export function openStore (
|
|
32
|
-
const file =
|
|
33
|
-
|
|
34
|
-
let data = readJson(file, null, atRest)
|
|
29
|
+
export function openStore (disk) {
|
|
30
|
+
const file = 'vault.json'
|
|
31
|
+
let data = readJson(disk, file, null)
|
|
35
32
|
if (!data || data.schemaVersion !== SCHEMA_VERSION) {
|
|
36
33
|
data = { schemaVersion: SCHEMA_VERSION, tree: newTree(), settings: {} }
|
|
37
34
|
}
|
|
38
35
|
// Se reescribe SIEMPRE al abrir: así un archivo de una instalación anterior
|
|
39
36
|
// (en claro) queda cifrado sin pedirle nada al usuario.
|
|
40
|
-
writeJson(file, data
|
|
41
|
-
const save = () => writeJson(file, data
|
|
37
|
+
writeJson(disk, file, data)
|
|
38
|
+
const save = () => writeJson(disk, file, data)
|
|
42
39
|
|
|
43
40
|
return {
|
|
44
41
|
get raw () { return data },
|
|
@@ -32,9 +32,8 @@
|
|
|
32
32
|
* hay conflictos ni hace falta un `seq`. Y **solo salen al absorberse** en el acta: nunca
|
|
33
33
|
* por reloj, o un aparato apagado perdería justo la que lo protege.
|
|
34
34
|
*/
|
|
35
|
-
import path from 'node:path'
|
|
36
35
|
import { verifyRenounce } from '@dotrino/identity/acta'
|
|
37
|
-
import { readJson, writeJson } from './
|
|
36
|
+
import { readJson, writeJson } from './disk.js'
|
|
38
37
|
|
|
39
38
|
/** Las operaciones que caben. La lista ES la regla: todas quitan, todas las firma su sujeto. */
|
|
40
39
|
export const OPS = Object.freeze(['renounce'])
|
|
@@ -57,10 +56,10 @@ export async function isAcceptable (e) {
|
|
|
57
56
|
* demás: sus entradas dicen qué aparatos se quedaron sin qué, y eso es material de la
|
|
58
57
|
* cuenta aunque no sea un secreto.
|
|
59
58
|
*/
|
|
60
|
-
export function openSubacta (
|
|
61
|
-
const file =
|
|
62
|
-
let data = readJson(file, null
|
|
63
|
-
const guardar = () => writeJson(file, data
|
|
59
|
+
export function openSubacta (disk) {
|
|
60
|
+
const file = 'subacta.json'
|
|
61
|
+
let data = readJson(disk, file, null) || { v: 1, entries: [] }
|
|
62
|
+
const guardar = () => writeJson(disk, file, data)
|
|
64
63
|
|
|
65
64
|
return {
|
|
66
65
|
/** Las entradas vigentes, tal cual se le pasan a `memberCan` y compañía. */
|
|
@@ -13,11 +13,9 @@
|
|
|
13
13
|
* vive el contenido de las apps y el perfil del usuario, que es exactamente lo
|
|
14
14
|
* que el ecosistema promete que no queda en claro en ningún disco.
|
|
15
15
|
*/
|
|
16
|
-
import path from 'node:path'
|
|
17
16
|
import * as core from '@dotrino/store/core'
|
|
18
17
|
import { resealStale } from '@dotrino/identity/content'
|
|
19
|
-
import { readJson, writeJson } from './
|
|
20
|
-
import { atRestFor } from './atrest.js'
|
|
18
|
+
import { readJson, writeJson } from './disk.js'
|
|
21
19
|
|
|
22
20
|
// El MISMO tope que acepta la página del almacén. Era 1000: una app que guarda más en un
|
|
23
21
|
// hilo (los compradores de facturero, por ejemplo) perdía aquí lo más viejo sin decir nada,
|
|
@@ -35,10 +33,9 @@ export const RESEALED_KEEP_MS = 366 * 24 * 60 * 60 * 1000
|
|
|
35
33
|
const MAX_SECURE_ITEMS = 2000
|
|
36
34
|
const MAX_SECURE_BLOB = 64 * 1024 // por campo sellado (meta y valor)
|
|
37
35
|
|
|
38
|
-
export function openThreadStore (
|
|
39
|
-
const file =
|
|
40
|
-
|
|
41
|
-
let data = readJson(file, null, atRest)
|
|
36
|
+
export function openThreadStore (disk) {
|
|
37
|
+
const file = 'threads.json'
|
|
38
|
+
let data = readJson(disk, file, null)
|
|
42
39
|
if (!data || typeof data !== 'object') data = { v: 1, threads: {}, opens: {} }
|
|
43
40
|
if (!data.threads) data.threads = {}
|
|
44
41
|
if (!data.opens) data.opens = {}
|
|
@@ -47,7 +44,7 @@ export function openThreadStore (dir) {
|
|
|
47
44
|
// primer aparato que todavía tenga la entrada.
|
|
48
45
|
if (!data.tombs) data.tombs = {}
|
|
49
46
|
core.pruneTombs(data.tombs, Date.now())
|
|
50
|
-
const save = () => writeJson(file, data
|
|
47
|
+
const save = () => writeJson(disk, file, data)
|
|
51
48
|
save() // reescribe al abrir: cifra lo que venía en claro
|
|
52
49
|
const budget = (n) => Math.min(Math.max(Number(n) || core.PAGE_BYTES, 1024), core.PAGE_BYTES)
|
|
53
50
|
const needKeys = (keys) => {
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
* con un `signData` de cortesía si hace falta. El daemon (vault.js) además puebla
|
|
21
21
|
* `me` antes de conectar.
|
|
22
22
|
*/
|
|
23
|
-
import { installNodeGlobals } from './node-globals.js'
|
|
24
23
|
|
|
25
24
|
const DEFAULT_PROXY = 'wss://proxy.dotrino.com'
|
|
26
25
|
|
|
@@ -31,13 +30,13 @@ export async function masterPubkeyOf (identity) {
|
|
|
31
30
|
/**
|
|
32
31
|
* Conecta el transporte y lo identifica con la maestra del vault.
|
|
33
32
|
* @param {Object} opts
|
|
34
|
-
* @param {import('@dotrino/identity/
|
|
35
|
-
* @param {
|
|
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
36
|
* @param {string} [opts.url] URL del proxy (default wss://proxy.dotrino.com).
|
|
37
37
|
* @returns {Promise<{ client, token:string, identify():Promise<void> }>}
|
|
38
38
|
*/
|
|
39
|
-
export async function createTransport ({ identity,
|
|
40
|
-
installNodeGlobals(dir)
|
|
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 }) {
|
|
41
40
|
// Import dinámico DESPUÉS de instalar los globals que el paquete usa.
|
|
42
41
|
// `WebSocketProxyClient` (la clase) y NO el helper `getWebSocketProxyClient`:
|
|
43
42
|
// ese es un SINGLETON de proceso, y con multi-perfil el vault necesita una
|