@dotrino/identity 0.115.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 +4 -1
- package/src/node.js +4 -180
- package/vault/core.js +66 -7
- package/vault/facade.js +200 -0
- package/vault/index.html +10 -0
- package/vault/remote.js +45 -5
- package/vault/vault.js +29 -58
- package/vault/vendor/compat/VERSION.txt +3 -0
- package/vault/vendor/compat/advisory.js +141 -0
- package/vault/vendor/compat/index.js +188 -0
- package/vault/vendor/compat/ranges.js +93 -0
- package/vault/vendor/opaque/VERSION.txt +1 -1
- package/vault/vendor/passmanager/VERSION.txt +3 -0
- package/vault/vendor/passmanager/crypto.js +167 -0
- package/vault/vendor/passmanager/fields.js +183 -0
- package/vault/vendor/passmanager/generate.js +74 -0
- package/vault/vendor/passmanager/import.js +156 -0
- package/vault/vendor/passmanager/index.js +27 -0
- package/vault/vendor/passmanager/match.js +137 -0
- package/vault/vendor/passmanager/model.js +332 -0
- package/vault/vendor/passmanager/pubkey.js +33 -0
- package/vault/vendor/passmanager/sealed/convert.js +72 -0
- package/vault/vendor/passmanager/sealed/device.js +211 -0
- package/vault/vendor/passmanager/sealed/entry.js +209 -0
- package/vault/vendor/passmanager/sealed/index.js +20 -0
- package/vault/vendor/passmanager/sealed/keys.js +120 -0
- package/vault/vendor/passmanager/sealed/recovery.js +136 -0
- package/vault/vendor/passmanager/sealed/store.js +508 -0
- package/vault/vendor/passmanager/session-cache.js +60 -0
- package/vault/vendor/passmanager/totp.js +111 -0
- package/vault/vendor/passmanager/transport/protocol.js +83 -0
- package/vault/vendor/passmanager/transport/proxy.js +112 -0
- package/vault/vendor/passmanager/transport/sealed.js +74 -0
- package/vault/vendor/passmanager/vault/approval.js +63 -0
- package/vault/vendor/passmanager/vault/errors.js +34 -0
- package/vault/vendor/passmanager/vault/guard.js +114 -0
- package/vault/vendor/passmanager/vault/interface.js +34 -0
- package/vault/vendor/passmanager/vault/local.js +213 -0
- package/vault/vendor/passmanager/vault/remote.js +85 -0
- package/vault/vendor/passmanager/vault/responder.js +192 -0
- package/vault/vendor/passmanager/vault/sealed-local.js +199 -0
- package/vault/vendor/passmanager/vault/sealed-responder.js +159 -0
- package/vault/vendor/passmanager/vault/sealed.js +362 -0
- package/vault/vendor/passmanager/webauthn.js +221 -0
- package/vault/vendor/proxy-client/VERSION.txt +1 -1
- package/vault/vendor/proxy-client/client.js +36 -0
- package/vault/vendor/roadmap/VERSION.txt +3 -0
- package/vault/vendor/roadmap/index.js +108 -0
- package/vault/vendor/roadmap/manifests.js +797 -0
- package/vault/vendor/store/VERSION.txt +3 -0
- package/vault/vendor/store/core.js +309 -0
- package/vault/vendor/vault/VERSION.txt +4 -1
- package/vault/vendor/vault/admin.js +437 -0
- package/vault/vendor/vault/browser.js +167 -0
- package/vault/vendor/vault/core/approvals.js +204 -0
- package/vault/vendor/vault/core/blocked.js +68 -0
- package/vault/vendor/vault/core/browserDisk.js +109 -0
- package/vault/vendor/vault/core/commKey.js +68 -0
- package/vault/vendor/vault/core/disk.js +65 -0
- package/vault/vendor/vault/core/libVersion.js +8 -0
- package/vault/vendor/vault/core/machines.js +66 -0
- package/vault/vendor/vault/core/notices.js +71 -0
- package/vault/vendor/vault/core/notifiable.js +73 -0
- package/vault/vendor/vault/core/passwords.js +125 -0
- package/vault/vendor/vault/core/sealKey.js +77 -0
- package/vault/vendor/vault/core/sealer.js +172 -0
- package/vault/vendor/vault/core/sealers.js +159 -0
- package/vault/vendor/vault/core/secretsStore.js +1326 -0
- package/vault/vendor/vault/core/sha256.js +69 -0
- package/vault/vendor/vault/core/store.js +68 -0
- package/vault/vendor/vault/core/subacta.js +101 -0
- package/vault/vendor/vault/core/threadStore.js +273 -0
- package/vault/vendor/vault/core/transport.js +270 -0
- package/vault/vendor/vault/core/vault.js +4414 -0
- package/vault/vendor/vault/enroll.js +8 -4
- package/vault/vendor/vault/procContext.js +97 -0
- package/vault/vendor/vault/revocation.js +67 -0
- package/vault/vendor/vault/sealed.js +84 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Las reglas de los hilos, en UN sitio: qué gana al mezclar, qué deja un borrado y cómo se
|
|
3
|
+
* compara lo que tiene cada lado.
|
|
4
|
+
*
|
|
5
|
+
* Lo importan DOS piezas que tienen que estar de acuerdo byte a byte: la página del almacén
|
|
6
|
+
* (`store.dotrino.com`, IndexedDB del navegador) y la bóveda (`dotrino-vault`, su
|
|
7
|
+
* `threads.json`). Si cada una llevara su copia, bastaría con que una cambiara el orden de
|
|
8
|
+
* una comparación para que las huellas no coincidieran nunca y la sincronización se
|
|
9
|
+
* repitiera para siempre sin decir por qué.
|
|
10
|
+
*
|
|
11
|
+
* Sin DOM y sin Node: solo `crypto.subtle` y `TextEncoder`, que están en los dos.
|
|
12
|
+
*
|
|
13
|
+
* Modelo:
|
|
14
|
+
* threads: { [threadKey]: Entry[] } Entry = { id, ts, ... } opaca
|
|
15
|
+
* tombs: { [threadKey]: { [id]: [ts, at] } } «la versión `ts` de `id` se borró en `at`»
|
|
16
|
+
*
|
|
17
|
+
* La lápida guarda el `ts` de lo que se borró, no la hora del borrado: así decidir si una
|
|
18
|
+
* copia que llega de otro aparato es la borrada no depende del reloj de nadie. Una versión
|
|
19
|
+
* posterior (ts mayor) sí entra: se editó en otro sitio después de borrarla aquí.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Tope de entradas por hilo que acepta cualquier lado. La página del almacén no pasa de aquí. */
|
|
23
|
+
export const MAX_PER_THREAD_LIMIT = 50000
|
|
24
|
+
|
|
25
|
+
/** Una lápida se olvida a los 180 días: un aparato apagado más tiempo que eso puede resucitar lo borrado. */
|
|
26
|
+
export const TOMB_TTL_MS = 180 * 24 * 60 * 60 * 1000
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Tope de bytes de una respuesta por partes. Con el cifrado de punta a punta (base64, ×4/3)
|
|
30
|
+
* y el sobre del mensaje, queda holgado bajo el 1 MB que corta el proxio.
|
|
31
|
+
*/
|
|
32
|
+
export const PAGE_BYTES = 400_000
|
|
33
|
+
|
|
34
|
+
const encoder = new TextEncoder()
|
|
35
|
+
export const byteLength = (value) => encoder.encode(typeof value === 'string' ? value : JSON.stringify(value)).length
|
|
36
|
+
|
|
37
|
+
const has = (obj, key) => obj != null && Object.hasOwn(obj, key)
|
|
38
|
+
|
|
39
|
+
/** `__proto__` como clave de hilo o id reescribiría el prototipo del mapa al asignar. */
|
|
40
|
+
export function validKey (key) {
|
|
41
|
+
return typeof key === 'string' && key.length > 0 && key !== '__proto__'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function assertKey (key) {
|
|
45
|
+
if (!validKey(key)) throw new Error('threadKey required')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const idOf = (entry) => String(entry.id)
|
|
49
|
+
|
|
50
|
+
// ----- huella -----
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* SHA-256 de «id TAB ts» por entrada, ordenadas por id (unidades UTF-16, igual en los dos
|
|
54
|
+
* lados). Cambia si entra, sale o se reescribe una entrada: reescribir sube `ts`.
|
|
55
|
+
* Las lápidas NO entran: se olvidan por fecha, y cada lado las olvida a su hora.
|
|
56
|
+
*/
|
|
57
|
+
export async function threadDigest (entries) {
|
|
58
|
+
const lines = entries
|
|
59
|
+
.map((e) => `${String(e.id)}\t${Number(e.ts) || 0}`)
|
|
60
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
61
|
+
const hash = await crypto.subtle.digest('SHA-256', encoder.encode(lines.join('\n')))
|
|
62
|
+
return Array.from(new Uint8Array(hash), (b) => b.toString(16).padStart(2, '0')).join('')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Huella de cada hilo con entradas (o solo de `keys`, si se pasan). */
|
|
66
|
+
export async function digestsOf (threads, keys) {
|
|
67
|
+
const out = {}
|
|
68
|
+
const list = Array.isArray(keys) ? keys.filter((k) => validKey(k) && has(threads, k)) : Object.keys(threads)
|
|
69
|
+
for (const k of list) {
|
|
70
|
+
const arr = threads[k]
|
|
71
|
+
if (!Array.isArray(arr) || arr.length === 0) continue
|
|
72
|
+
out[k] = { count: arr.length, digest: await threadDigest(arr) }
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ----- lápidas -----
|
|
78
|
+
|
|
79
|
+
export function tombOf (tombs, k, id) {
|
|
80
|
+
return has(tombs, k) && has(tombs[k], String(id)) ? tombs[k][String(id)] : undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** ¿Esta versión de la entrada es una que se borró (o anterior)? */
|
|
84
|
+
export function isBuried (tombs, k, entry) {
|
|
85
|
+
const t = tombOf(tombs, k, entry.id)
|
|
86
|
+
return !!t && (Number(entry.ts) || 0) <= t[0]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Anota un borrado. Si ya había lápida, se queda la versión más nueva y la fecha más vieja. */
|
|
90
|
+
export function bury (tombs, k, id, ts, at) {
|
|
91
|
+
const key = String(id)
|
|
92
|
+
if (!validKey(k) || key === '__proto__') return false
|
|
93
|
+
const t = has(tombs, k) ? tombs[k] : (tombs[k] = {})
|
|
94
|
+
const prev = has(t, key) ? t[key] : null
|
|
95
|
+
const next = prev ? [Math.max(prev[0], ts), Math.min(prev[1], at)] : [ts, at]
|
|
96
|
+
if (prev && prev[0] === next[0] && prev[1] === next[1]) return false
|
|
97
|
+
t[key] = next
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function unbury (tombs, k, id) {
|
|
102
|
+
const key = String(id)
|
|
103
|
+
if (!has(tombs, k) || !has(tombs[k], key)) return false
|
|
104
|
+
delete tombs[k][key]
|
|
105
|
+
if (Object.keys(tombs[k]).length === 0) delete tombs[k]
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Olvida las lápidas de hace más de `TOMB_TTL_MS`. */
|
|
110
|
+
export function pruneTombs (tombs, now) {
|
|
111
|
+
let changed = false
|
|
112
|
+
for (const k of Object.keys(tombs)) {
|
|
113
|
+
for (const [id, [, at]] of Object.entries(tombs[k])) {
|
|
114
|
+
if (at < now - TOMB_TTL_MS) { delete tombs[k][id]; changed = true }
|
|
115
|
+
}
|
|
116
|
+
if (Object.keys(tombs[k]).length === 0) { delete tombs[k]; changed = true }
|
|
117
|
+
}
|
|
118
|
+
return changed
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Aplica lápidas que llegan de otro lado: las anota y quita las entradas que entierran.
|
|
123
|
+
* `incoming`: { [threadKey]: [[id, ts, at], ...] }. Devuelve los hilos cuyas entradas cambiaron.
|
|
124
|
+
*/
|
|
125
|
+
export function applyTombs (threads, tombs, incoming, now) {
|
|
126
|
+
const changed = new Set()
|
|
127
|
+
let tombsChanged = false
|
|
128
|
+
for (const [k, list] of Object.entries(incoming || {})) {
|
|
129
|
+
if (!validKey(k) || !Array.isArray(list)) continue
|
|
130
|
+
for (const row of list) {
|
|
131
|
+
if (!Array.isArray(row) || row.length < 2) continue
|
|
132
|
+
const [id, ts, at] = row
|
|
133
|
+
if (typeof ts !== 'number') continue
|
|
134
|
+
if (bury(tombs, k, id, ts, typeof at === 'number' ? at : now)) tombsChanged = true
|
|
135
|
+
}
|
|
136
|
+
if (!has(threads, k)) continue
|
|
137
|
+
const kept = threads[k].filter((e) => !isBuried(tombs, k, e))
|
|
138
|
+
if (kept.length === threads[k].length) continue
|
|
139
|
+
if (kept.length) threads[k] = kept; else delete threads[k]
|
|
140
|
+
changed.add(k)
|
|
141
|
+
}
|
|
142
|
+
return { changed, tombsChanged }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ----- escribir -----
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Mezcla entradas que llegan. `merge`: gana la de `ts` mayor. `upsert`: con el mismo `ts`
|
|
149
|
+
* gana la que llega (es una escritura de su dueño, no una copia vieja). Lo enterrado no
|
|
150
|
+
* entra. Devuelve los hilos que cambiaron.
|
|
151
|
+
*/
|
|
152
|
+
export function mergeEntries (threads, tombs, incoming, { mode = 'merge', max = MAX_PER_THREAD_LIMIT } = {}) {
|
|
153
|
+
const changed = new Set()
|
|
154
|
+
for (const [k, arr] of Object.entries(incoming || {})) {
|
|
155
|
+
if (!validKey(k) || !Array.isArray(arr) || arr.length === 0) continue
|
|
156
|
+
const current = has(threads, k) ? threads[k] : []
|
|
157
|
+
const byId = new Map()
|
|
158
|
+
for (const e of current) if (e && e.id != null) byId.set(idOf(e), e)
|
|
159
|
+
let touched = false
|
|
160
|
+
for (const e of arr) {
|
|
161
|
+
if (!e || typeof e !== 'object' || e.id == null) continue
|
|
162
|
+
if (isBuried(tombs, k, e)) continue
|
|
163
|
+
const prev = byId.get(idOf(e))
|
|
164
|
+
const ts = Number(e.ts) || 0
|
|
165
|
+
const prevTs = Number(prev?.ts) || 0
|
|
166
|
+
if (!prev || ts > prevTs || (mode === 'upsert' && ts === prevTs)) { byId.set(idOf(e), e); touched = true }
|
|
167
|
+
}
|
|
168
|
+
if (!touched) continue
|
|
169
|
+
const merged = Array.from(byId.values()).sort((a, b) => (Number(a.ts) || 0) - (Number(b.ts) || 0))
|
|
170
|
+
if (merged.length > max) merged.splice(0, merged.length - max)
|
|
171
|
+
threads[k] = merged
|
|
172
|
+
changed.add(k)
|
|
173
|
+
}
|
|
174
|
+
return changed
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Escritura directa de una entrada (`appendMessage`): mezcla superficial con la que ya
|
|
179
|
+
* estaba y quita su lápida, porque escribirla de nuevo es decir que vuelve.
|
|
180
|
+
*/
|
|
181
|
+
export function writeEntry (threads, tombs, k, entry, { max, now, newId }) {
|
|
182
|
+
assertKey(k)
|
|
183
|
+
if (!entry || typeof entry !== 'object') throw new Error('entry required')
|
|
184
|
+
if (entry.id == null) entry.id = newId()
|
|
185
|
+
if (!entry.ts) entry.ts = now
|
|
186
|
+
const tombCleared = unbury(tombs, k, entry.id)
|
|
187
|
+
const arr = has(threads, k) ? threads[k] : (threads[k] = [])
|
|
188
|
+
const i = arr.findIndex((e) => e && e.id != null && idOf(e) === idOf(entry))
|
|
189
|
+
if (i >= 0) arr[i] = { ...arr[i], ...entry }; else arr.push(entry)
|
|
190
|
+
if (arr.length > max) arr.splice(0, arr.length - max)
|
|
191
|
+
return { entry, tombCleared }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Borra una entrada y deja su lápida. */
|
|
195
|
+
export function removeEntry (threads, tombs, k, id, now) {
|
|
196
|
+
if (!validKey(k) || id == null || !has(threads, k)) return 0
|
|
197
|
+
const arr = threads[k]
|
|
198
|
+
const i = arr.findIndex((e) => e && e.id != null && idOf(e) === String(id))
|
|
199
|
+
if (i < 0) return 0
|
|
200
|
+
const [gone] = arr.splice(i, 1)
|
|
201
|
+
bury(tombs, k, gone.id, Number(gone.ts) || 0, now)
|
|
202
|
+
if (arr.length === 0) delete threads[k]
|
|
203
|
+
return 1
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Borra un hilo entero: una lápida por cada entrada que tenía. */
|
|
207
|
+
export function removeWholeThread (threads, tombs, k, now) {
|
|
208
|
+
if (!validKey(k) || !has(threads, k)) return 0
|
|
209
|
+
const arr = threads[k]
|
|
210
|
+
for (const e of arr) if (e && e.id != null) bury(tombs, k, e.id, Number(e.ts) || 0, now)
|
|
211
|
+
delete threads[k]
|
|
212
|
+
return arr.length
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ----- leer por partes -----
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Índice de los hilos `keys`: `items` [[id, ts]] y `tombs` [[id, ts, at]], por páginas de
|
|
219
|
+
* `maxBytes` como mucho. `cursor` { k, o }: hilo y posición donde siguió la página anterior.
|
|
220
|
+
* Siempre sale al menos una fila, o no se avanzaría nunca.
|
|
221
|
+
*/
|
|
222
|
+
export function indexPage (threads, tombs, keys, cursor, maxBytes) {
|
|
223
|
+
const indexes = {}
|
|
224
|
+
let bytes = 0
|
|
225
|
+
let rows = 0
|
|
226
|
+
let o = Number(cursor?.o) || 0
|
|
227
|
+
for (let ki = Number(cursor?.k) || 0; ki < keys.length; ki++, o = 0) {
|
|
228
|
+
const k = keys[ki]
|
|
229
|
+
if (!validKey(k)) continue
|
|
230
|
+
const arr = has(threads, k) ? threads[k] : []
|
|
231
|
+
const tl = has(tombs, k) ? Object.entries(tombs[k]) : []
|
|
232
|
+
const total = arr.length + tl.length
|
|
233
|
+
if (total === 0) continue
|
|
234
|
+
const idx = has(indexes, k) ? indexes[k] : (indexes[k] = { items: [], tombs: [] })
|
|
235
|
+
for (; o < total; o++) {
|
|
236
|
+
const isItem = o < arr.length
|
|
237
|
+
const row = isItem ? [arr[o].id, Number(arr[o].ts) || 0] : [tl[o - arr.length][0], ...tl[o - arr.length][1]]
|
|
238
|
+
const size = byteLength(row) + 1
|
|
239
|
+
if (rows > 0 && bytes + size > maxBytes) return { indexes, next: { k: ki, o } }
|
|
240
|
+
;(isItem ? idx.items : idx.tombs).push(row)
|
|
241
|
+
bytes += size
|
|
242
|
+
rows++
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return { indexes, next: null }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Entradas pedidas por id: `refs` { [threadKey]: [id, ...] }. Lo que no cabe en `maxBytes`
|
|
250
|
+
* vuelve en `rest` para pedirlo después; lo que no existe no vuelve.
|
|
251
|
+
*/
|
|
252
|
+
export function entriesPage (threads, refs, maxBytes) {
|
|
253
|
+
const out = {}
|
|
254
|
+
const rest = {}
|
|
255
|
+
let bytes = 0
|
|
256
|
+
let count = 0
|
|
257
|
+
let full = false
|
|
258
|
+
for (const [k, ids] of Object.entries(refs || {})) {
|
|
259
|
+
if (!validKey(k) || !Array.isArray(ids)) continue
|
|
260
|
+
if (full) { rest[k] = ids.slice(); continue }
|
|
261
|
+
if (!has(threads, k)) continue
|
|
262
|
+
const byId = new Map(threads[k].filter((e) => e && e.id != null).map((e) => [idOf(e), e]))
|
|
263
|
+
for (let i = 0; i < ids.length; i++) {
|
|
264
|
+
const e = byId.get(String(ids[i]))
|
|
265
|
+
if (!e) continue
|
|
266
|
+
const size = byteLength(e)
|
|
267
|
+
if (count > 0 && bytes + size > maxBytes) { rest[k] = ids.slice(i); full = true; break }
|
|
268
|
+
;(has(out, k) ? out[k] : (out[k] = [])).push(e)
|
|
269
|
+
bytes += size
|
|
270
|
+
count++
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return { threads: out, rest: Object.keys(rest).length ? rest : null }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Lápidas de los ids pedidos (`refs`) y todas las de los hilos `keys`, como filas [id, ts, at]. */
|
|
277
|
+
export function pickTombs (tombs, refs, keys) {
|
|
278
|
+
const out = {}
|
|
279
|
+
const add = (k, id) => {
|
|
280
|
+
const t = tombOf(tombs, k, id)
|
|
281
|
+
if (!t) return
|
|
282
|
+
const list = has(out, k) ? out[k] : (out[k] = [])
|
|
283
|
+
if (!list.some((row) => row[0] === String(id))) list.push([String(id), ...t])
|
|
284
|
+
}
|
|
285
|
+
for (const [k, ids] of Object.entries(refs || {})) if (validKey(k) && Array.isArray(ids)) for (const id of ids) add(k, id)
|
|
286
|
+
for (const k of keys || []) if (validKey(k) && has(tombs, k)) for (const id of Object.keys(tombs[k])) add(k, id)
|
|
287
|
+
return out
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ----- aperturas -----
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Mezcla el contador de aperturas por app: el mayor de cada lado. No suma — sumar contaría
|
|
294
|
+
* dos veces lo que ya se mezcló — y así mezclar dos veces da lo mismo que una.
|
|
295
|
+
*/
|
|
296
|
+
export function mergeOpens (opens, incoming) {
|
|
297
|
+
let changed = false
|
|
298
|
+
for (const [app, rec] of Object.entries(incoming || {})) {
|
|
299
|
+
if (!validKey(app) || !rec || typeof rec !== 'object') continue
|
|
300
|
+
const count = Number(rec.count) || 0
|
|
301
|
+
const ts = Number(rec.ts) || 0
|
|
302
|
+
const prev = has(opens, app) ? opens[app] : null
|
|
303
|
+
const next = prev ? { count: Math.max(prev.count || 0, count), ts: Math.max(prev.ts || 0, ts) } : { count, ts }
|
|
304
|
+
if (prev && prev.count === next.count && prev.ts === next.ts) continue
|
|
305
|
+
opens[app] = next
|
|
306
|
+
changed = true
|
|
307
|
+
}
|
|
308
|
+
return changed
|
|
309
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/vault@0.
|
|
1
|
+
Copia vendorizada de @dotrino/vault@0.86.0 (dotrino-vault/lib/src/{admin,b64,browser,core/approvals,core/blocked,core/browserDisk,core/commKey,core/disk,core/libVersion,core/machines,core/notices,core/notifiable,core/passwords,core/sealKey,core/sealer,core/sealers,core/secretsStore,core/sha256,core/store,core/subacta,core/threadStore,core/transport,core/vault,enroll,index,loginClient,passwordLogins,procContext,protocol,revocation,sealed}.js).
|
|
2
2
|
NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
|
|
3
|
+
browser.js es la bóveda de esta pestaña: el MISMO núcleo que el binario (core/), con
|
|
4
|
+
el disco en IndexedDB. Importa @dotrino/{compat,roadmap,passmanager,store/core} y
|
|
5
|
+
@dotrino/identity/{facade,machine,content}, todos por el import map de index.html.
|
|
3
6
|
index.js importa ./enroll.js y ./protocol.js (relativos, van en esta misma copia),
|
|
4
7
|
@dotrino/identity/{capabilities,acta} (= ../../{capabilities,acta}.js) y
|
|
5
8
|
@dotrino/proxy-client (= ../proxy-client/), todos por el import map de index.html.
|