@dotrino/vaultd 0.114.0 → 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.
- package/package.json +2 -1
- package/src/approvals.js +7 -0
- package/src/client.js +26 -9
- package/src/threadStore.js +60 -27
- package/src/vault.js +20 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.116.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/client.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* estrictamente (firmado por la maestra que vio en el QR, y para SU clave) antes de
|
|
12
12
|
* guardarlo. La maestra nunca sale del vault.
|
|
13
13
|
*/
|
|
14
|
-
import { makeDeviceKey, signWithDevice, verifyDelegation, verifyDeviceSig, makePairingCode, commitCode, pubkeyId } from '@dotrino/identity/capabilities'
|
|
14
|
+
import { makeDeviceKey, makeDeviceEncKey, importDeviceEncKey, signWithDevice, verifyDelegation, verifyDeviceSig, makePairingCode, commitCode, pubkeyId } from '@dotrino/identity/capabilities'
|
|
15
|
+
import { myContentKey, encryptWithCek, decryptWithKeyring } from '@dotrino/identity/content'
|
|
15
16
|
import { sealersOf } from '@dotrino/identity/acta'
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -61,20 +62,25 @@ function waitFor (client, predicate, timeoutMs = 30000) {
|
|
|
61
62
|
* @param {string} [opts.label]
|
|
62
63
|
* @param {(c:{deviceId:string,code:string})=>void} [opts.onChallenge] Para MOSTRAR el código que el usuario tipea en la bóveda.
|
|
63
64
|
* @param {number} [opts.approveTimeoutMs] Cuánto esperar la aprobación humana (def 3 min).
|
|
64
|
-
* @returns {Promise<{ device, cert, iss:string }>} GUARDAR `device` (incluye
|
|
65
|
+
* @returns {Promise<{ device, cert, iss:string, acta }>} GUARDAR `device` (incluye las privadas de firma y de
|
|
66
|
+
* cifrado), `cert` y `acta` (trae la clave de contenido envuelta para este aparato: sin ella el
|
|
67
|
+
* almacén de la bóveda no se puede usar). `iss` = qr.iss verificado.
|
|
65
68
|
*/
|
|
66
69
|
export async function enroll ({ qr, label = '', dir, onChallenge, approveTimeoutMs = 180000 } = {}) {
|
|
67
70
|
if (!qr?.iss || !qr?.proxy || !qr?.token || !qr?.sn) throw new Error('invalid qr (v2): missing iss/proxy/token/sn')
|
|
68
71
|
const client = await freshClient({ proxyUrl: qr.proxy, dir })
|
|
69
72
|
try {
|
|
70
73
|
const device = await makeDeviceKey({ label })
|
|
74
|
+
// La llave de CIFRADO del aparato: con su pública en el acta, la bóveda le envuelve la
|
|
75
|
+
// clave de contenido al admitirlo. Sin ella no podría usar el almacén, que solo entra cifrado.
|
|
76
|
+
const enc = await makeDeviceEncKey()
|
|
71
77
|
const myDeviceId = (await pubkeyId(device.publickey)).slice(0, 8).toUpperCase().replace(/(.{4})(.{4})/, '$1-$2')
|
|
72
78
|
// ESTE dispositivo genera el código y lo MUESTRA; solo viaja su COMPROMISO. El vault
|
|
73
79
|
// lo aprende cuando un humano lo tipea, y solo firma el cert si el compromiso coincide.
|
|
74
80
|
const code = makePairingCode()
|
|
75
81
|
const commit = await commitCode({ code, dpub: device.publickey, sn: qr.sn })
|
|
76
82
|
// ENROLL firmado con D = prueba de posesión (un token robado ya no basta).
|
|
77
|
-
const data = { op: 'enroll', dpub: device.publickey, token: qr.token, sn: qr.sn, commit, label, ts: Date.now() }
|
|
83
|
+
const data = { op: 'enroll', dpub: device.publickey, encPub: enc.encPublickey, token: qr.token, sn: qr.sn, commit, label, ts: Date.now() }
|
|
78
84
|
const { signature } = await signWithDevice({ privateJwk: device.privateJwk, data })
|
|
79
85
|
|
|
80
86
|
const enrolled = new Promise((resolve, reject) => {
|
|
@@ -102,7 +108,7 @@ export async function enroll ({ qr, label = '', dir, onChallenge, approveTimeout
|
|
|
102
108
|
if (!v.ok) throw new Error('invalid cert: ' + v.reason)
|
|
103
109
|
if (res.cert.sub !== device.publickey) throw new Error('cert issued for a different device')
|
|
104
110
|
// OJO: devolvemos qr.iss (la maestra que el usuario VIO), NO res.iss.
|
|
105
|
-
return { device, cert: res.cert, iss: qr.iss }
|
|
111
|
+
return { device: { ...device, encPublickey: enc.encPublickey, encPrivateJwk: enc.encPrivateJwk }, cert: res.cert, iss: qr.iss, acta: res.acta }
|
|
106
112
|
} finally { client.close() }
|
|
107
113
|
}
|
|
108
114
|
|
|
@@ -171,17 +177,28 @@ export async function requestRenew ({ masterPubkey, proxyUrl, device, cert, dir
|
|
|
171
177
|
} finally { client.close() }
|
|
172
178
|
}
|
|
173
179
|
|
|
174
|
-
/**
|
|
175
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Llama un método del almacén de la bóveda (scope vault:store), CIFRADO de punta a punta con
|
|
182
|
+
* la clave de contenido del perfil: la bóveda no acepta el almacén en claro, porque el proxio
|
|
183
|
+
* no cifra. `acta` es la que devolvió `enroll` (o una más nueva): de su `keyring` sale la
|
|
184
|
+
* clave envuelta para este aparato.
|
|
185
|
+
*/
|
|
186
|
+
export async function requestStore ({ masterPubkey, proxyUrl, device, cert, acta, method, args, dir } = {}) {
|
|
187
|
+
if (!device?.encPrivateJwk) throw Object.assign(new Error('this device has no encryption key: enrol it again'), { code: 'no-enc-key' })
|
|
188
|
+
const myEncPrivateKey = await importDeviceEncKey(device.encPrivateJwk)
|
|
189
|
+
const mine = await myContentKey({ keyring: acta?.keyring, myPub: device.publickey, myEncPrivateKey })
|
|
190
|
+
if (!mine) throw Object.assign(new Error('this device does not hold the profile content key yet'), { code: 'no-content-key' })
|
|
191
|
+
const enc = await encryptWithCek({ cek: mine.cek, gen: mine.gen, plaintext: JSON.stringify(args ?? {}) })
|
|
176
192
|
const client = await freshClient({ proxyUrl, dir })
|
|
177
193
|
try {
|
|
178
|
-
const data = { op: 'store', method,
|
|
194
|
+
const data = { op: 'store', method, enc, publickey: device.publickey, ts: Date.now() }
|
|
179
195
|
const { signature } = await signWithDevice({ privateJwk: device.privateJwk, data })
|
|
180
196
|
const pending = waitFor(client, (p) => p.type === MSG.STORE_RESULT || p.type === MSG.ERROR)
|
|
181
197
|
client.sendByPubkey(masterPubkey, { type: MSG.STORE, data, signature, cert })
|
|
182
198
|
const res = await pending
|
|
183
|
-
if (res.type === MSG.ERROR) throw new Error(res.error)
|
|
184
|
-
|
|
199
|
+
if (res.type === MSG.ERROR) throw Object.assign(new Error(res.error), res.code ? { code: res.code } : {})
|
|
200
|
+
if (!res.result?.__enc) throw Object.assign(new Error('the vault replied to the store without encrypting it'), { code: 'vault-reply-unsealed' })
|
|
201
|
+
return JSON.parse(await decryptWithKeyring({ envelope: res.result.__enc, keyring: acta.keyring, myPub: device.publickey, myEncPrivateKey }))
|
|
185
202
|
} finally { client.close() }
|
|
186
203
|
}
|
|
187
204
|
|
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
|
|
@@ -949,6 +951,16 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
949
951
|
// guardar un archivo en tu bóveda (dueño, 2026-09-03). El proxio tiene `guarda`
|
|
950
952
|
// para lo suyo y no debe poder cambiar quién eres.
|
|
951
953
|
if (PROFILE_METHODS.has(d.method)) return await handleProfile(from, p, d)
|
|
954
|
+
// EL ALMACÉN SOLO ENTRA CIFRADO. Lo que va por aquí —hilos de las apps, contraseñas,
|
|
955
|
+
// el `me` heredado— viaja por el proxio, que no cifra; en claro lo leería quien lo
|
|
956
|
+
// opere. Los clientes del ecosistema cifran desde @dotrino/identity 0.91.0, y hasta
|
|
957
|
+
// entonces uno sin la clave de contenido mandaba en claro «como antes»: aceptarlo aquí
|
|
958
|
+
// era dejar ese camino abierto para cualquier cliente viejo. El perfil (arriba) va
|
|
959
|
+
// aparte a propósito: lo privado viaja en sobres y lo público es público.
|
|
960
|
+
if (!d.enc) {
|
|
961
|
+
audit('rejected', { what: 'store', method: d.method, reason: 'unsealed' })
|
|
962
|
+
return reply(from, { type: MSG.ERROR, error: 'store: requests must be sealed with the profile content key', code: 'store-unsealed' })
|
|
963
|
+
}
|
|
952
964
|
// CANDADO del perfil (contraseña opcional): solo frena EDITAR el perfil heredado
|
|
953
965
|
// (`profileSet`, el `me` plano). Un dispositivo enrolado puede seguir firmando,
|
|
954
966
|
// leyendo y guardando contenido.
|
|
@@ -968,15 +980,11 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
968
980
|
if (!await actaAllows(from, chk, chk.scope === SCOPE.READ ? SCOPE.READ : SCOPE.STORE, 'store')) return
|
|
969
981
|
try {
|
|
970
982
|
// CIFRADO de punta a punta con la clave de contenido del perfil: el proxy transporta
|
|
971
|
-
// pero no ve nada de lo que el usuario guarda.
|
|
972
|
-
//
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
cek = await identity.contentKey?.().catch(() => null)
|
|
977
|
-
if (!cek) return reply(from, { type: MSG.ERROR, error: 'store: this vault does not hold the profile content key' })
|
|
978
|
-
args = JSON.parse(await identity.openContent(d.enc))
|
|
979
|
-
}
|
|
983
|
+
// pero no ve nada de lo que el usuario guarda. Se abre aquí con la clave de la bóveda
|
|
984
|
+
// (que también es miembro) y la respuesta vuelve igual.
|
|
985
|
+
const cek = await identity.contentKey()
|
|
986
|
+
if (!cek) return reply(from, { type: MSG.ERROR, error: 'store: this vault does not hold the profile content key', code: 'vault-no-content-key' })
|
|
987
|
+
const args = JSON.parse(await identity.openContent(d.enc))
|
|
980
988
|
const result = await threads.methods[d.method](args)
|
|
981
989
|
// Que un aparato ESCRIBA en tu bóveda queda anotado. Antes solo se auditaba el
|
|
982
990
|
// rechazo, así que la bitácora contaba quién entró pero no qué hizo después.
|
|
@@ -985,11 +993,8 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
985
993
|
if (!STORE_READ_METHODS.has(d.method)) {
|
|
986
994
|
audit('store', { device: await deviceIdOf(chk.device), method: d.method })
|
|
987
995
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
return reply(from, { type: MSG.STORE_RESULT, method: d.method, result: { __enc: enc } })
|
|
991
|
-
}
|
|
992
|
-
reply(from, { type: MSG.STORE_RESULT, method: d.method, result })
|
|
996
|
+
const enc = await identity.sealContent(JSON.stringify(result ?? null))
|
|
997
|
+
reply(from, { type: MSG.STORE_RESULT, method: d.method, result: { __enc: enc } })
|
|
993
998
|
} catch (e) { reply(from, { type: MSG.ERROR, error: e.message }) }
|
|
994
999
|
}
|
|
995
1000
|
|