@dotrino/identity 0.114.1 → 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.
Files changed (81) hide show
  1. package/package.json +7 -1
  2. package/src/index.js +9 -1
  3. package/src/node.js +4 -180
  4. package/vault/acta.js +5 -1
  5. package/vault/core.js +95 -15
  6. package/vault/facade.js +200 -0
  7. package/vault/index.html +10 -0
  8. package/vault/machine.js +95 -0
  9. package/vault/remote.js +54 -9
  10. package/vault/vault.js +29 -58
  11. package/vault/vendor/compat/VERSION.txt +3 -0
  12. package/vault/vendor/compat/advisory.js +141 -0
  13. package/vault/vendor/compat/index.js +188 -0
  14. package/vault/vendor/compat/ranges.js +93 -0
  15. package/vault/vendor/opaque/VERSION.txt +1 -1
  16. package/vault/vendor/passmanager/VERSION.txt +3 -0
  17. package/vault/vendor/passmanager/crypto.js +167 -0
  18. package/vault/vendor/passmanager/fields.js +183 -0
  19. package/vault/vendor/passmanager/generate.js +74 -0
  20. package/vault/vendor/passmanager/import.js +156 -0
  21. package/vault/vendor/passmanager/index.js +27 -0
  22. package/vault/vendor/passmanager/match.js +137 -0
  23. package/vault/vendor/passmanager/model.js +332 -0
  24. package/vault/vendor/passmanager/pubkey.js +33 -0
  25. package/vault/vendor/passmanager/sealed/convert.js +72 -0
  26. package/vault/vendor/passmanager/sealed/device.js +211 -0
  27. package/vault/vendor/passmanager/sealed/entry.js +209 -0
  28. package/vault/vendor/passmanager/sealed/index.js +20 -0
  29. package/vault/vendor/passmanager/sealed/keys.js +120 -0
  30. package/vault/vendor/passmanager/sealed/recovery.js +136 -0
  31. package/vault/vendor/passmanager/sealed/store.js +508 -0
  32. package/vault/vendor/passmanager/session-cache.js +60 -0
  33. package/vault/vendor/passmanager/totp.js +111 -0
  34. package/vault/vendor/passmanager/transport/protocol.js +83 -0
  35. package/vault/vendor/passmanager/transport/proxy.js +112 -0
  36. package/vault/vendor/passmanager/transport/sealed.js +74 -0
  37. package/vault/vendor/passmanager/vault/approval.js +63 -0
  38. package/vault/vendor/passmanager/vault/errors.js +34 -0
  39. package/vault/vendor/passmanager/vault/guard.js +114 -0
  40. package/vault/vendor/passmanager/vault/interface.js +34 -0
  41. package/vault/vendor/passmanager/vault/local.js +213 -0
  42. package/vault/vendor/passmanager/vault/remote.js +85 -0
  43. package/vault/vendor/passmanager/vault/responder.js +192 -0
  44. package/vault/vendor/passmanager/vault/sealed-local.js +199 -0
  45. package/vault/vendor/passmanager/vault/sealed-responder.js +159 -0
  46. package/vault/vendor/passmanager/vault/sealed.js +362 -0
  47. package/vault/vendor/passmanager/webauthn.js +221 -0
  48. package/vault/vendor/proxy-client/VERSION.txt +1 -1
  49. package/vault/vendor/proxy-client/client.js +36 -0
  50. package/vault/vendor/roadmap/VERSION.txt +3 -0
  51. package/vault/vendor/roadmap/index.js +108 -0
  52. package/vault/vendor/roadmap/manifests.js +797 -0
  53. package/vault/vendor/store/VERSION.txt +3 -0
  54. package/vault/vendor/store/core.js +309 -0
  55. package/vault/vendor/vault/VERSION.txt +4 -1
  56. package/vault/vendor/vault/admin.js +437 -0
  57. package/vault/vendor/vault/browser.js +167 -0
  58. package/vault/vendor/vault/core/approvals.js +204 -0
  59. package/vault/vendor/vault/core/blocked.js +68 -0
  60. package/vault/vendor/vault/core/browserDisk.js +109 -0
  61. package/vault/vendor/vault/core/commKey.js +68 -0
  62. package/vault/vendor/vault/core/disk.js +65 -0
  63. package/vault/vendor/vault/core/libVersion.js +8 -0
  64. package/vault/vendor/vault/core/machines.js +66 -0
  65. package/vault/vendor/vault/core/notices.js +71 -0
  66. package/vault/vendor/vault/core/notifiable.js +73 -0
  67. package/vault/vendor/vault/core/passwords.js +125 -0
  68. package/vault/vendor/vault/core/sealKey.js +77 -0
  69. package/vault/vendor/vault/core/sealer.js +172 -0
  70. package/vault/vendor/vault/core/sealers.js +159 -0
  71. package/vault/vendor/vault/core/secretsStore.js +1326 -0
  72. package/vault/vendor/vault/core/sha256.js +69 -0
  73. package/vault/vendor/vault/core/store.js +68 -0
  74. package/vault/vendor/vault/core/subacta.js +101 -0
  75. package/vault/vendor/vault/core/threadStore.js +273 -0
  76. package/vault/vendor/vault/core/transport.js +270 -0
  77. package/vault/vendor/vault/core/vault.js +4414 -0
  78. package/vault/vendor/vault/enroll.js +8 -4
  79. package/vault/vendor/vault/procContext.js +97 -0
  80. package/vault/vendor/vault/revocation.js +67 -0
  81. package/vault/vendor/vault/sealed.js +84 -0
@@ -0,0 +1,3 @@
1
+ Copia vendorizada de @dotrino/store/core@0.12.3 (dotrino-store/store/{core}.js).
2
+ NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
3
+ Las reglas del almacén (lápidas, páginas): las mismas en el store y en la bóveda.
@@ -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.84.0 (dotrino-vault/lib/src/{index,enroll,protocol,passwordLogins,loginClient,b64}.js).
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.