@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
package/vault/remote.js
CHANGED
|
@@ -281,11 +281,53 @@ function vaultError (p) {
|
|
|
281
281
|
return e
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
/**
|
|
285
|
+
* LA BÓVEDA DE ESTE MISMO NAVEGADOR, sin dar la vuelta por el proxio.
|
|
286
|
+
*
|
|
287
|
+
* Cuando esta pestaña o esta extensión ES la bóveda (`@dotrino/vault`, `startBrowserVault`),
|
|
288
|
+
* lo que el aparato le pide a su bóveda —administrar, aprobar, firmar— va directo al núcleo
|
|
289
|
+
* que corre aquí. Es el mostrador local del binario en el navegador: el MISMO protocolo, la
|
|
290
|
+
* misma firma y el mismo certificado, comprobados igual. Alcanzar el atajo no autoriza nada.
|
|
291
|
+
*
|
|
292
|
+
* `open(onReply)` → `{ send(payload), close() }`. Lo registra quien levanta la bóveda.
|
|
293
|
+
*/
|
|
294
|
+
const localVaults = new Map()
|
|
295
|
+
export function registerLocalVault (master, open) {
|
|
296
|
+
if (typeof master !== 'string' || typeof open !== 'function') throw new Error('registerLocalVault: master and open are required')
|
|
297
|
+
localVaults.set(master, open)
|
|
298
|
+
return () => { if (localVaults.get(master) === open) localVaults.delete(master) }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Un cliente con la forma del del proxio que habla con la bóveda local. */
|
|
302
|
+
function localClient (open) {
|
|
303
|
+
const handlers = new Set()
|
|
304
|
+
const chan = open((reply) => { for (const h of [...handlers]) { try { h('local', reply) } catch (_) {} } })
|
|
305
|
+
return {
|
|
306
|
+
connect: async () => {},
|
|
307
|
+
identifyAs: async () => {},
|
|
308
|
+
on (ev, fn) {
|
|
309
|
+
if (ev !== 'message') return () => {}
|
|
310
|
+
handlers.add(fn)
|
|
311
|
+
return () => handlers.delete(fn)
|
|
312
|
+
},
|
|
313
|
+
sendByPubkey (_to, payload) { chan.send(payload) },
|
|
314
|
+
close () { handlers.clear(); chan.close() }
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** El cliente para hablar con `master`: el local si esa bóveda corre aquí, y si no el del proxio. */
|
|
319
|
+
async function clientFor (proxy, master) {
|
|
320
|
+
const open = master ? localVaults.get(master) : null
|
|
321
|
+
if (open) return localClient(open)
|
|
286
322
|
const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
|
|
287
323
|
const client = new WebSocketProxyClient({ url: proxy, enableWebRTC: false, autoReconnect: false })
|
|
288
324
|
await client.connect()
|
|
325
|
+
return client
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function vaultRpc ({ master, proxy, device, cert, acta = null, sendType, okType, data, onRevoked, timeoutMs = 15000 }) {
|
|
329
|
+
if (!master || !proxy || !(device?.privateJwk || device?.privateKey || typeof device?.sign === 'function') || !cert) throw new Error('missing pairing data')
|
|
330
|
+
const client = await clientFor(proxy, master)
|
|
289
331
|
try {
|
|
290
332
|
try { await identifyAsDevice(client, device, { cert, acta }) } catch (_) { /* sin identify seguimos: solo perdemos la cola */ }
|
|
291
333
|
const signed = { ...data, publickey: device.publickey, ts: Date.now(), ...(cleanMachine(device.machine) ? { machine: cleanMachine(device.machine) } : {}) }
|
|
@@ -336,9 +378,7 @@ async function vaultRpc ({ master, proxy, device, cert, acta = null, sendType, o
|
|
|
336
378
|
*/
|
|
337
379
|
export async function checkMembership ({ master, proxy, device, onRevoked, timeoutMs = 12000 } = {}) {
|
|
338
380
|
if (!master || !proxy || !(device?.privateJwk || device?.privateKey || typeof device?.sign === 'function')) throw new Error('missing device data')
|
|
339
|
-
const
|
|
340
|
-
const client = new WebSocketProxyClient({ url: proxy, enableWebRTC: false, autoReconnect: false })
|
|
341
|
-
await client.connect()
|
|
381
|
+
const client = await clientFor(proxy, master)
|
|
342
382
|
try {
|
|
343
383
|
// Identificarse hace además que el proxy entregue lo ENCOLADO (un aviso de cuando
|
|
344
384
|
// estaba apagado, si todavía está dentro de las 24 h).
|
package/vault/vault.js
CHANGED
|
@@ -185,9 +185,9 @@ import { useBackend as usePeerBackend } from './peerStore.js'
|
|
|
185
185
|
core.onSyncStatus((p) => broadcast('sync', p))
|
|
186
186
|
core.onVaultEvent((p) => broadcast('vault', p))
|
|
187
187
|
|
|
188
|
-
// ---- Modo SELF: este navegador actúa como bóveda
|
|
189
|
-
//
|
|
190
|
-
//
|
|
188
|
+
// ---- Modo SELF: este navegador actúa como bóveda ----
|
|
189
|
+
// Corre la MISMA bóveda que el binario (`startBrowserVault`): emparejar, firmar, cajones,
|
|
190
|
+
// aprobaciones, consola remota, inicios de sesión. Solo UN iframe por origin es el activo
|
|
191
191
|
// (navigator.locks): la pestaña VISIBLE sostiene el lock; al pasar a background lo
|
|
192
192
|
// libera y otra pestaña visible lo toma. Así varias apps abiertas no compiten.
|
|
193
193
|
const SELF_FLAG = 'dotrino.self-vault.enabled' // persistido en localStorage (kv)
|
|
@@ -208,79 +208,47 @@ import { useBackend as usePeerBackend } from './peerStore.js'
|
|
|
208
208
|
return /^wss?:\/\//.test(p || '') ? p : null
|
|
209
209
|
} catch (_) { return null }
|
|
210
210
|
})()
|
|
211
|
-
let daemon = null //
|
|
211
|
+
let daemon = null // la bóveda (`startBrowserVault`) cuando ESTE iframe es el activo
|
|
212
212
|
let _lockResolver = null // resolver del callback del lock (libera al resolverlo)
|
|
213
213
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
// `admitMember`/`profileActa` son opcionales para el mostrador de enrolamiento, pero sin
|
|
217
|
-
// ellos aprobar emitiría el cert SIN meter al dispositivo en el acta: se pasan también.
|
|
218
|
-
const selfIdentity = {
|
|
219
|
-
get me () { return core.me },
|
|
220
|
-
signData: (data) => handlers.signData({ data }),
|
|
221
|
-
signDelegation: (sub, scope, opts) => handlers.signDelegation({ sub, scope, ...(opts || {}) }),
|
|
222
|
-
listDelegations: () => handlers.listDelegations({}),
|
|
223
|
-
revokeDelegation: (nonce) => handlers.revokeDelegation({ nonce }),
|
|
224
|
-
revokeDevice: (sub) => handlers.revokeDevice({ sub }),
|
|
225
|
-
admitMember: (m) => handlers.admitMember(m),
|
|
226
|
-
profileActa: () => handlers.profileActa({}),
|
|
227
|
-
// Camino A (`mode: 'adopt'`): la bóveda se queda con la cuenta que trae el aparato,
|
|
228
|
-
// y para eso tiene que poder ENTRAR en su acta. Sin esto, adoptar fallaba con un
|
|
229
|
-
// «no es una función» en vez de con un error del protocolo.
|
|
230
|
-
joinProfile: (acta) => handlers.joinProfile({ acta })
|
|
231
|
-
}
|
|
214
|
+
// La dirección `nombre@AB12-CD34-EF56` sale del acta de la cuenta.
|
|
215
|
+
const selfIdentity = { profileActa: () => handlers.profileActa({}) }
|
|
232
216
|
|
|
233
217
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
* `logins-unavailable`, que es distinto de «contraseña incorrecta» y por eso se ve.
|
|
239
|
-
*
|
|
240
|
-
* Cuelga del PERFIL ACTIVO: los inicios de sesión son de una cuenta, no de este
|
|
241
|
-
* navegador, y dos cuentas en el mismo equipo no comparten usuarios.
|
|
242
|
-
*
|
|
243
|
-
* Se carga solo aquí, y no arriba, porque arrastra el OPAQUE en WASM (~270 KB): el
|
|
244
|
-
* iframe lo cargan las ~30 apps del ecosistema y solo esta pestaña es bóveda.
|
|
245
|
-
*
|
|
246
|
-
* Lo que guarda aguanta lo mismo que el disco del daemon: con una copia de este
|
|
247
|
-
* `localStorage` se pueden probar contraseñas sin límite contra el registro de OPAQUE.
|
|
248
|
-
* Lo único que aguanta ahí es que la contraseña sea larga (`temporary-access.md` §4).
|
|
218
|
+
* LOS INICIOS DE SESIÓN QUE ESTA PESTAÑA GUARDABA ANTES de correr la bóveda entera. Ahora
|
|
219
|
+
* viven en el almacén de la bóveda (IndexedDB cifrado, uno por perfil); lo de antes se le
|
|
220
|
+
* pasa una vez y se borra de aquí. Si está y no parsea, REVIENTA a propósito: tomarlo por
|
|
221
|
+
* vacío perdería los inicios de sesión de la cuenta sin que nadie se enterara.
|
|
249
222
|
*/
|
|
250
|
-
async function
|
|
251
|
-
const { createLoginDesk } = await import('@dotrino/vault/password-logins')
|
|
223
|
+
async function legacyLogins () {
|
|
252
224
|
const { id: pid } = await handlers.currentProfile()
|
|
253
|
-
if (!pid)
|
|
225
|
+
if (!pid) return null
|
|
254
226
|
const key = `dotrino.identity.p.${pid}.self-vault.logins`
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// Si está y no parsea, REVIENTA aquí a propósito: devolver `null` haría nacer el
|
|
259
|
-
// escritorio vacío, y el primer guardado se llevaría por delante todos los inicios
|
|
260
|
-
// de sesión de esta cuenta sin que nadie se enterara.
|
|
261
|
-
return raw ? JSON.parse(raw) : null
|
|
262
|
-
},
|
|
263
|
-
save: (s) => kv.setItem(key, JSON.stringify(s))
|
|
264
|
-
})
|
|
227
|
+
const raw = kv.getItem(key)
|
|
228
|
+
if (!raw) return null
|
|
229
|
+
return { state: JSON.parse(raw), drop: () => kv.removeItem(key) }
|
|
265
230
|
}
|
|
266
231
|
|
|
267
232
|
async function startSelfDaemon () {
|
|
268
233
|
if (daemon) return
|
|
269
234
|
try {
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
235
|
+
// LA MISMA BÓVEDA QUE EL BINARIO (`@dotrino/vault/browser`): el núcleo entero, con el
|
|
236
|
+
// disco en IndexedDB. Import dinámico: el iframe lo cargan las ~30 apps y solo esta
|
|
237
|
+
// pestaña es bóveda; además aísla un fallo del vendor del arranque de la identidad.
|
|
238
|
+
const { startBrowserVault } = await import('@dotrino/vault/browser')
|
|
239
|
+
daemon = await startBrowserVault({
|
|
240
|
+
core,
|
|
274
241
|
...(selfProxyUrl ? { proxyUrl: selfProxyUrl } : {}),
|
|
275
|
-
|
|
242
|
+
legacyLogins,
|
|
243
|
+
onPendingChange: () => broadcast('selfVault', { pending: daemon ? daemon.listPending() : [] }),
|
|
244
|
+
log: (...a) => console.log(...a)
|
|
276
245
|
})
|
|
277
|
-
daemon.onPendingChange(() => broadcast('selfVault', { pending: daemon.listPending() }))
|
|
278
246
|
broadcast('selfVault', { running: true })
|
|
279
247
|
} catch (e) { daemon = null; broadcast('selfVault', { error: e?.message || String(e) }) }
|
|
280
248
|
}
|
|
281
249
|
function stopSelfDaemon () {
|
|
282
250
|
if (!daemon) return
|
|
283
|
-
try { daemon.close() } catch {}
|
|
251
|
+
try { daemon.close()?.catch?.(() => {}) } catch {}
|
|
284
252
|
daemon = null
|
|
285
253
|
broadcast('selfVault', { running: false })
|
|
286
254
|
}
|
|
@@ -340,6 +308,9 @@ import { useBackend as usePeerBackend } from './peerStore.js'
|
|
|
340
308
|
// agentes siempre, sin depender de qué pestaña sostenga el lock del daemon.
|
|
341
309
|
listVaultDevices: async () => {
|
|
342
310
|
if (kv.getItem(SELF_FLAG) !== '1') return handlers.listVaultDevices({})
|
|
311
|
+
// Con la bóveda encendida en ESTA pestaña, la lista sale de ella (por el camino
|
|
312
|
+
// local): trae lo mismo que la del binario. Sin ella encendida, se lee lo guardado.
|
|
313
|
+
if (daemon) return handlers.listVaultDevices({})
|
|
343
314
|
const { issued, revoked } = await handlers.listDelegations({})
|
|
344
315
|
const now = Date.now()
|
|
345
316
|
const bySub = new Map()
|
|
@@ -491,7 +462,7 @@ import { useBackend as usePeerBackend } from './peerStore.js'
|
|
|
491
462
|
selfVaultLoginRemove: async ({ user } = {}) => {
|
|
492
463
|
const d = pidaDaemon()
|
|
493
464
|
const fila = d.listLogins().find((x) => x.user === user)
|
|
494
|
-
const r = d.removeLogin({ user })
|
|
465
|
+
const r = await d.removeLogin({ user })
|
|
495
466
|
if (r?.ok && fila?.pub) {
|
|
496
467
|
try { await handlers.revokeDevice({ sub: fila.pub }) } catch (e) {
|
|
497
468
|
throw Object.assign(new Error(`the login is gone but its key is still in the record: ${e.message}`), { code: 'revoke-failed' })
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* advisory.js — EL AVISO A LA RED: «actualiza estos componentes».
|
|
3
|
+
*
|
|
4
|
+
* Punto 3 del dueño (2026-09-04): *«como desarrolladores vemos que una versión falla o es
|
|
5
|
+
* incompatible con otra, tenemos un método de avisar a la red que debe actualizar tales o
|
|
6
|
+
* tales componentes»*.
|
|
7
|
+
*
|
|
8
|
+
* QUIÉN LO FIRMA, y esto es lo que lo ordena todo: **los que desarrollan**. La
|
|
9
|
+
* compatibilidad es política del CÓDIGO, no de la cuenta (decidido el mismo día), así que
|
|
10
|
+
* el ancla de confianza —la pública que valida el aviso— **viaja dentro de la build**. No
|
|
11
|
+
* hay lista de confianza que se pueda cambiar a distancia: para cambiar en quién confías
|
|
12
|
+
* hay que instalar otra versión, que es exactamente lo que significa «política del código».
|
|
13
|
+
*
|
|
14
|
+
* CÓMO VIAJA: por la red que ya existe, de boca en boca. Cada pieza le pasa a la siguiente
|
|
15
|
+
* el aviso más nuevo que tenga. **No hay servidor de avisos ni consulta a Dotrino**: nadie
|
|
16
|
+
* llama a casa, nada depende de que un servicio nuestro esté encendido, y no hay un sitio
|
|
17
|
+
* donde se vea quién tiene qué versión.
|
|
18
|
+
*
|
|
19
|
+
* LO QUE ESTO ES, dicho sin adornos: un aviso firmado por nosotros puede dejar sin trabajar
|
|
20
|
+
* a una versión instalada en la máquina de otro. Es lo que se pidió y es el precio de que
|
|
21
|
+
* un fallo detectado hoy frene hoy. Lo que lo acota:
|
|
22
|
+
*
|
|
23
|
+
* · **Solo hacia adelante** (`seq`): un aviso viejo no se puede reproducir para deshacer
|
|
24
|
+
* uno nuevo.
|
|
25
|
+
* · **Un aviso nuevo REEMPLAZA la lista entera**, así que un aviso equivocado se corrige
|
|
26
|
+
* con otro aviso. Si solo pudiera sumar, una equivocación sería para siempre.
|
|
27
|
+
* · **No concede nada.** Lo que la build trae marcado como roto sigue roto aunque el
|
|
28
|
+
* aviso no lo mencione: quitar por la red lo que el código afirma sería justo el
|
|
29
|
+
* agujero (`CLAUDE.md`, «nada de repliegues»).
|
|
30
|
+
* · **Se ve.** El administrador enseña el aviso y su fecha. Un freno que no se explica
|
|
31
|
+
* es otro silencio, que es de lo que venimos huyendo.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* JSON canónico (claves ordenadas, recursivo). Es una COPIA deliberada del de
|
|
36
|
+
* `@dotrino/proxy-client`: este paquete no depende de nadie —lo importan las ~30 apps y el
|
|
37
|
+
* proxio— y la firma tiene que dar los mismos bytes en las dos puntas. Si algún día
|
|
38
|
+
* cambian, cambian los dos: hay un test que compara la salida contra la del pilar.
|
|
39
|
+
*/
|
|
40
|
+
export function canonicalStringify (value) {
|
|
41
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value)
|
|
42
|
+
if (Array.isArray(value)) return '[' + value.map(canonicalStringify).join(',') + ']'
|
|
43
|
+
const keys = Object.keys(value).sort()
|
|
44
|
+
return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalStringify(value[k])).join(',') + '}'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const enc = (s) => new TextEncoder().encode(s)
|
|
48
|
+
const b64 = (buf) => {
|
|
49
|
+
const b = new Uint8Array(buf)
|
|
50
|
+
let s = ''
|
|
51
|
+
for (let i = 0; i < b.length; i++) s += String.fromCharCode(b[i])
|
|
52
|
+
return btoa(s)
|
|
53
|
+
}
|
|
54
|
+
const fromB64 = (str) => {
|
|
55
|
+
const bin = atob(str)
|
|
56
|
+
const out = new Uint8Array(bin.length)
|
|
57
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** El cuerpo que se firma: todo menos la firma. */
|
|
62
|
+
export const advisoryBody = (a) => {
|
|
63
|
+
const { sig, ...body } = a || {}
|
|
64
|
+
return body
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** ¿Tiene forma de aviso? No dice si es de fiar, solo si se puede juzgar. */
|
|
68
|
+
export function isAdvisory (a) {
|
|
69
|
+
return !!a && a.op === 'compat.advisory' && Number.isInteger(a.seq) && a.seq >= 0 &&
|
|
70
|
+
typeof a.issued === 'number' && Array.isArray(a.broken) && typeof a.sig === 'string' &&
|
|
71
|
+
a.broken.every((b) => b && typeof b.product === 'string' && Array.isArray(b.versions) &&
|
|
72
|
+
b.versions.every((v) => typeof v === 'string'))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* FIRMA un aviso. Lo corre quien publica, no quien lo recibe: aquí está para que el
|
|
77
|
+
* formato lo defina un solo sitio y no dos implementaciones que se separan.
|
|
78
|
+
*/
|
|
79
|
+
export async function signAdvisory ({ seq, broken, privateJwk, issued = Date.now() }) {
|
|
80
|
+
const body = { op: 'compat.advisory', seq, issued, broken }
|
|
81
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
82
|
+
'jwk', privateJwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign'])
|
|
83
|
+
const sig = await globalThis.crypto.subtle.sign(
|
|
84
|
+
{ name: 'ECDSA', hash: 'SHA-256' }, key, enc(canonicalStringify(body)))
|
|
85
|
+
return { ...body, sig: b64(sig) }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* ¿Lo firmó la llave de release que trae MI build? `publickey` es un JWK en texto, igual
|
|
90
|
+
* que en el resto del ecosistema.
|
|
91
|
+
*/
|
|
92
|
+
export async function verifyAdvisory ({ advisory, publickey }) {
|
|
93
|
+
if (!isAdvisory(advisory) || typeof publickey !== 'string') return false
|
|
94
|
+
try {
|
|
95
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
96
|
+
'jwk', JSON.parse(publickey), { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify'])
|
|
97
|
+
return await globalThis.crypto.subtle.verify(
|
|
98
|
+
{ name: 'ECDSA', hash: 'SHA-256' }, key, fromB64(advisory.sig),
|
|
99
|
+
enc(canonicalStringify(advisoryBody(advisory))))
|
|
100
|
+
} catch (_) { return false }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* ¿ME QUEDO CON EL QUE LLEGA? Solo hacia adelante y solo si lo firma quien yo digo.
|
|
105
|
+
* Devuelve el aviso vigente (el nuevo o el que ya tenía) y por qué.
|
|
106
|
+
*/
|
|
107
|
+
export async function adoptAdvisory ({ current, incoming, publickey }) {
|
|
108
|
+
if (!isAdvisory(incoming)) return { advisory: current || null, adopted: false, reason: 'no-es-un-aviso' }
|
|
109
|
+
if (current && incoming.seq <= current.seq) {
|
|
110
|
+
return { advisory: current, adopted: false, reason: 'no-es-mas-nuevo' }
|
|
111
|
+
}
|
|
112
|
+
if (!(await verifyAdvisory({ advisory: incoming, publickey }))) {
|
|
113
|
+
return { advisory: current || null, adopted: false, reason: 'firma-invalida' }
|
|
114
|
+
}
|
|
115
|
+
return { advisory: incoming, adopted: true, reason: current ? 'mas-nuevo' : 'primero' }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* LA LISTA DE ROTAS QUE VALE HOY: la del código MÁS la del aviso.
|
|
120
|
+
*
|
|
121
|
+
* Se suman, no se sustituyen: el aviso puede añadir lo que se descubrió después de esta
|
|
122
|
+
* build, pero **no puede quitar** lo que esta build ya afirma. Que la red pudiera
|
|
123
|
+
* desmarcar algo que el código da por roto sería el repliegue de manual.
|
|
124
|
+
*/
|
|
125
|
+
export function brokenNow ({ baked = [], advisory = null } = {}) {
|
|
126
|
+
return advisory && isAdvisory(advisory) ? [...baked, ...advisory.broken] : [...baked]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Lo que el administrador enseña del aviso vigente (§14: se ve, no se esconde). */
|
|
130
|
+
export function advisorySummary (advisory) {
|
|
131
|
+
if (!isAdvisory(advisory)) return null
|
|
132
|
+
return {
|
|
133
|
+
seq: advisory.seq,
|
|
134
|
+
issued: advisory.issued,
|
|
135
|
+
components: advisory.broken.map((b) => ({
|
|
136
|
+
product: b.product, versions: b.versions, why: b.why || null, fix: b.fix || null
|
|
137
|
+
}))
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default { signAdvisory, verifyAdvisory, adoptAdvisory, brokenNow, isAdvisory, advisoryBody, advisorySummary, canonicalStringify }
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dotrino/compat — QUÉ VERSIÓN ERES, Y SI PODEMOS TRABAJAR.
|
|
3
|
+
*
|
|
4
|
+
* EL PROBLEMA QUE VIENE A RESOLVER, dicho con los tres casos que costaron días:
|
|
5
|
+
*
|
|
6
|
+
* · El apagón del 1-2 de septiembre: endurecer la bóveda dejó mudos a `content`, a los
|
|
7
|
+
* selladores y al bot social durante un día entero. Ningún fallo era un error: eran
|
|
8
|
+
* reintentos cada 60 segundos contra alguien que ya no les entendía.
|
|
9
|
+
* · `@dotrino/env` pedía `^0.33.2` con la librería en 0.60: enrolar contestaba
|
|
10
|
+
* `invalid cert: no-acta`, que es verdad y no es la causa. La causa era la versión.
|
|
11
|
+
* · La bóveda se quedó MUDA tres días porque su respuesta pasaba el tope del proxio: el
|
|
12
|
+
* socket moría con un 1009 y no lo decía nadie.
|
|
13
|
+
*
|
|
14
|
+
* Los tres son el mismo fallo: **una incompatibilidad se manifiesta como SILENCIO**. El
|
|
15
|
+
* que llama no sabe si el otro está apagado, ocupado o hablando otro idioma, así que
|
|
16
|
+
* reintenta para siempre — y el que atiende no sabe que le están hablando.
|
|
17
|
+
*
|
|
18
|
+
* Aquí no hay red, ni disco, ni cripto: son funciones puras. Decidir es de este módulo;
|
|
19
|
+
* anunciar y refunfuñar es de quien lo usa. Y por eso no tiene dependencias — lo importan
|
|
20
|
+
* los ~30 PWA, los daemons y el proxio, y un pilar dentro de otro se cuela anidado a cada
|
|
21
|
+
* consumidor (ver `PENDIENTES.md`, «pilar anidado»).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Lo que responde `check`. El `code` es lo que se compara; el texto puede cambiar. */
|
|
25
|
+
import { satisfies } from './ranges.js'
|
|
26
|
+
|
|
27
|
+
export const OK = 'ok'
|
|
28
|
+
export const INCOMPATIBLE_PROTOCOL = 'incompatible-protocol'
|
|
29
|
+
export const BROKEN_PEER = 'broken-peer'
|
|
30
|
+
export const UNDECLARED = 'undeclared'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* ESTO INFORMA, NO BLOQUEA (dueño, 2026-09-04, corrigiendo el punto 3 del mismo día:
|
|
34
|
+
* *«quizás el aviso de incompatibilidad no debería ser bloqueante, pero sí visible»*).
|
|
35
|
+
*
|
|
36
|
+
* Y es mejor diseño, por cuatro razones que conviene tener escritas para no volver atrás:
|
|
37
|
+
*
|
|
38
|
+
* · **No hay interruptor remoto.** Un aviso que solo informa se puede firmar y repartir
|
|
39
|
+
* sin que nadie pueda dejar a otro sin bóveda desde fuera.
|
|
40
|
+
* · **No hay trampa de orden.** Si bloqueara, el día que una pieza empieza a comprobar
|
|
41
|
+
* dejaría de hablar con todas las que aún no anuncian: habría que desplegar en un orden
|
|
42
|
+
* exacto. Informando se enciende donde sea y cuando sea.
|
|
43
|
+
* · **Falla del lado seguro.** Bloquear es código nuevo decidiendo si algo funciona: un
|
|
44
|
+
* rango mal escrito o una errata en un manifiesto pasaría de aviso falso a caída real.
|
|
45
|
+
* · **Es lo que dicen los tres incidentes.** En el apagón del 1-2 de septiembre, en el
|
|
46
|
+
* `^0.33.2` y en la bóveda muda, lo que faltó fue ENTERARSE, no parar. Parar no habría
|
|
47
|
+
* arreglado ninguno.
|
|
48
|
+
*
|
|
49
|
+
* Lo que se pierde y se dice: un par realmente incompatible sigue medio funcionando. Pero
|
|
50
|
+
* medio funcionando **con un cartel que explica por qué** es otra cosa — y lo que no cuadra
|
|
51
|
+
* falla solo, con el aviso pegado al error (`annotate`).
|
|
52
|
+
*
|
|
53
|
+
* Quien consuma esto **no debe** convertir un `compatible: false` en un «no atiendo». Si
|
|
54
|
+
* algún día se decide bloquear, se decide arriba y se escribe allí, no aquí.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/** ¿Tiene forma de anuncio? No dice si es compatible, solo si se puede juzgar. */
|
|
58
|
+
export function isDeclaration (d) {
|
|
59
|
+
return !!d && typeof d.product === 'string' && d.product.length > 0 &&
|
|
60
|
+
typeof d.version === 'string' && d.version.length > 0 &&
|
|
61
|
+
Number.isInteger(d.protocol) &&
|
|
62
|
+
Array.isArray(d.speaks) && d.speaks.length > 0 && d.speaks.every(Number.isInteger)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Lo que se pone en el sobre firmado de `identify`. Corto, porque viaja en cada conexión. */
|
|
66
|
+
export function declare ({ product, version, protocol, speaks }) {
|
|
67
|
+
if (!Number.isInteger(protocol)) throw new Error('compat: protocol must be an integer')
|
|
68
|
+
const habla = speaks && speaks.length ? [...new Set(speaks)].sort((a, b) => a - b) : [protocol]
|
|
69
|
+
if (!habla.includes(protocol)) throw new Error('compat: speaks must include protocol')
|
|
70
|
+
const d = { product: String(product), version: String(version), protocol, speaks: habla }
|
|
71
|
+
if (!isDeclaration(d)) throw new Error('compat: incomplete declaration')
|
|
72
|
+
return d
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* ¿ESTE PAR ESTÁ EN LA LISTA DE ROTAS?
|
|
77
|
+
*
|
|
78
|
+
* `versions` acepta lo mismo que un rango del manifiesto (`ranges.js`): una versión
|
|
79
|
+
* exacta, un `>=`, un intervalo `a - b`, o una lista que es un O. El dueño pidió rangos
|
|
80
|
+
* para los manifiestos (2026-09-04) y aquí se usa el MISMO comparador, no otro: dos formas
|
|
81
|
+
* de decir «esta versión entra» acaban discrepando, y esa discrepancia no hace ruido.
|
|
82
|
+
*
|
|
83
|
+
* Lo que no se entiende no coincide — no marca roto y tampoco absuelve: solo no dice nada.
|
|
84
|
+
*/
|
|
85
|
+
export function isBroken (lista, { product, version }) {
|
|
86
|
+
for (const b of lista || []) {
|
|
87
|
+
if (b?.product !== product) continue
|
|
88
|
+
if (satisfies(version, b.versions)) return b
|
|
89
|
+
}
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* ¿ESTA PAREJA CUADRA?
|
|
95
|
+
*
|
|
96
|
+
* Devuelve un DICTAMEN, no un permiso: `compatible` dice si cuadran, y nadie debe usarlo
|
|
97
|
+
* para dejar de atender (ver arriba). Se enseña, se registra y se le pega a los errores.
|
|
98
|
+
*
|
|
99
|
+
* **JUZGA EL QUE ENTRA** (dueño, 2026-09-04): *«el que define si es compatible o no es la
|
|
100
|
+
* versión que entra, ya que la versión antigua de un producto no tiene idea con qué es o no
|
|
101
|
+
* compatible»*. Exacto: una build de hace tres meses no sabe nada de lo que vino después.
|
|
102
|
+
* Así que esto NO es simétrico — miro si YO te entiendo y si TÚ estás en mi lista de rotas;
|
|
103
|
+
* lo que tú creas de mí no entra en la cuenta.
|
|
104
|
+
*
|
|
105
|
+
* Y de ahí sale por qué avisar es la otra mitad: si el viejo no puede juzgar, tampoco puede
|
|
106
|
+
* enterarse solo. El que ve la incompatibilidad es el único que puede decírselo.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} o.mine mi declaración
|
|
109
|
+
* @param {object} o.theirs la del otro (o null si no dijo nada)
|
|
110
|
+
* @param {Array} [o.broken] mi lista de rotas (la del código más la del aviso de la red)
|
|
111
|
+
* @returns {{compatible:boolean, code:string, reason:string, peer:object|null}}
|
|
112
|
+
*/
|
|
113
|
+
export function check ({ mine, theirs, broken = [] } = {}) {
|
|
114
|
+
if (!isDeclaration(mine)) throw new Error('compat: my own declaration is not valid')
|
|
115
|
+
|
|
116
|
+
if (!isDeclaration(theirs)) {
|
|
117
|
+
return {
|
|
118
|
+
compatible: false,
|
|
119
|
+
code: UNDECLARED,
|
|
120
|
+
peer: null,
|
|
121
|
+
reason: 'the other side does not say what it is or which version it runs'
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const roto = isBroken(broken, theirs)
|
|
126
|
+
if (roto) {
|
|
127
|
+
return {
|
|
128
|
+
compatible: false,
|
|
129
|
+
code: BROKEN_PEER,
|
|
130
|
+
peer: theirs,
|
|
131
|
+
reason: `${theirs.product} ${theirs.version} is known to be broken: ${roto.why || 'no reason recorded'}` +
|
|
132
|
+
(roto.fix ? ` — ${roto.fix}` : '')
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!mine.speaks.includes(theirs.protocol)) {
|
|
137
|
+
return {
|
|
138
|
+
compatible: false,
|
|
139
|
+
code: INCOMPATIBLE_PROTOCOL,
|
|
140
|
+
peer: theirs,
|
|
141
|
+
reason: `${mine.product} ${mine.version} speaks protocol ${mine.speaks.join(', ')} and ` +
|
|
142
|
+
`${theirs.product} ${theirs.version} speaks ${theirs.protocol}: update ${theirs.product}`
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { compatible: true, code: OK, peer: theirs, reason: '' }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* PEGA EL AVISO AL ERROR QUE YA OCURRE, y esto es lo que de verdad ahorra el día perdido.
|
|
151
|
+
*
|
|
152
|
+
* El caso real: `@dotrino/env` pedía `^0.33.2` con la librería en 0.60, y enrolar contestaba
|
|
153
|
+
* `invalid cert: no-acta`. Es verdad y no es la causa — la causa era la versión, y encontrarlo
|
|
154
|
+
* costó horas. Con esto el mismo error sale:
|
|
155
|
+
*
|
|
156
|
+
* invalid cert: no-acta — heads up: vault 0.33.2 against vaultd 0.106.2 …
|
|
157
|
+
*
|
|
158
|
+
* El mensaje original no se toca: se le añade detrás. Nadie que empareje por texto se rompe
|
|
159
|
+
* si compara por prefijo, y quien compare por `code` no se entera (ver «los errores son un
|
|
160
|
+
* contrato»).
|
|
161
|
+
*/
|
|
162
|
+
export function annotate (message, verdict) {
|
|
163
|
+
if (!verdict || verdict.compatible !== false) return message
|
|
164
|
+
return `${message} — heads up: ${verdict.reason}`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* EL AVISO QUE SE MANDA CUANDO NO SE PUEDE TRABAJAR (punto 3 del dueño: «se comunican,
|
|
169
|
+
* pero solo para alertar la incompatibilidad»).
|
|
170
|
+
*
|
|
171
|
+
* Lleva las dos declaraciones porque el que lo recibe suele ser el que NO sabe que está
|
|
172
|
+
* roto —es el viejo—, así que hay que decirle qué es él, qué somos nosotros y qué hacer.
|
|
173
|
+
* Sin esto el rechazo sería otro silencio, que es de lo que venimos huyendo.
|
|
174
|
+
*/
|
|
175
|
+
export function incompatibleNotice ({ mine, theirs, verdict }) {
|
|
176
|
+
return {
|
|
177
|
+
op: 'incompatible',
|
|
178
|
+
code: verdict.code,
|
|
179
|
+
reason: verdict.reason,
|
|
180
|
+
mine: { product: mine.product, version: mine.version, protocol: mine.protocol, speaks: mine.speaks },
|
|
181
|
+
yours: theirs && isDeclaration(theirs)
|
|
182
|
+
? { product: theirs.product, version: theirs.version, protocol: theirs.protocol }
|
|
183
|
+
: null,
|
|
184
|
+
fix: 'update the older side; nothing here will work until the two versions match'
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export default { declare, check, isBroken, isDeclaration, incompatibleNotice, annotate, OK, INCOMPATIBLE_PROTOCOL, BROKEN_PEER, UNDECLARED }
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ranges.js — RANGOS DE VERSIONES, acotados a lo que el ecosistema usa de verdad.
|
|
3
|
+
*
|
|
4
|
+
* El dueño pidió que los manifiestos digan **qué rangos son compatibles con qué**
|
|
5
|
+
* (2026-09-04), así que hace falta comparar versiones. Mi objeción de la mañana seguía en
|
|
6
|
+
* pie —un comparador de semver mal escrito es peor que no tenerlo, porque falla en silencio
|
|
7
|
+
* y del lado que no toca— y la respuesta no es no tenerlo, es acotarlo:
|
|
8
|
+
*
|
|
9
|
+
* · **`x.y.z` y nada más.** Sin prereleases, sin `+build`, sin `^` ni `~`. Todas las
|
|
10
|
+
* versiones del ecosistema tienen esa forma; no se inventa soporte para lo que no hay.
|
|
11
|
+
* · **Lo que no se entiende NO pasa.** Un rango con una forma rara devuelve `false`, no
|
|
12
|
+
* «bueno, adelante». Es la regla de siempre: si falta lo que hace falta para decidir,
|
|
13
|
+
* se para.
|
|
14
|
+
*
|
|
15
|
+
* Formas admitidas, y son todas:
|
|
16
|
+
*
|
|
17
|
+
* "0.106.2" exacta
|
|
18
|
+
* "0.106.0+" ABIERTO hacia arriba: de esa en adelante
|
|
19
|
+
* ">=0.106.0" lo mismo, en el otro idioma. También > <= <
|
|
20
|
+
* "0.100.0 - 0.106.2" intervalo, extremos incluidos
|
|
21
|
+
* "0.100.0 - *" abierto por el extremo de arriba
|
|
22
|
+
* "*" cualquiera
|
|
23
|
+
* [">=0.5.0", "0.4.2"] una lista es un O
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const PARTES = /^(\d+)\.(\d+)\.(\d+)$/
|
|
27
|
+
|
|
28
|
+
/** `[major, minor, patch]`, o `null` si no tiene la forma. */
|
|
29
|
+
export function parseVersion (v) {
|
|
30
|
+
const m = PARTES.exec(String(v || '').trim())
|
|
31
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** -1, 0 o 1. Lanza si alguna no tiene forma: comparar basura no puede dar un resultado. */
|
|
35
|
+
export function compareVersions (a, b) {
|
|
36
|
+
const x = parseVersion(a)
|
|
37
|
+
const y = parseVersion(b)
|
|
38
|
+
if (!x || !y) throw new Error(`compat: cannot compare versions "${a}" and "${b}"`)
|
|
39
|
+
for (let i = 0; i < 3; i++) {
|
|
40
|
+
if (x[i] !== y[i]) return x[i] < y[i] ? -1 : 1
|
|
41
|
+
}
|
|
42
|
+
return 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const OPS = [
|
|
46
|
+
['>=', (c) => c >= 0],
|
|
47
|
+
['<=', (c) => c <= 0],
|
|
48
|
+
['>', (c) => c > 0],
|
|
49
|
+
['<', (c) => c < 0]
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* ¿`version` cae dentro de `range`?
|
|
54
|
+
* @param {string} version
|
|
55
|
+
* @param {string|string[]} range
|
|
56
|
+
* @returns {boolean} `false` también cuando el rango no se entiende
|
|
57
|
+
*/
|
|
58
|
+
export function satisfies (version, range) {
|
|
59
|
+
if (Array.isArray(range)) return range.some((r) => satisfies(version, r))
|
|
60
|
+
if (typeof range !== 'string') return false
|
|
61
|
+
const r = range.trim()
|
|
62
|
+
if (!r) return false
|
|
63
|
+
if (r === '*') return true
|
|
64
|
+
if (!parseVersion(version)) return false
|
|
65
|
+
|
|
66
|
+
// ABIERTO HACIA ARRIBA: «0.106.0+» es «de esa en adelante». Es la forma que más se usa
|
|
67
|
+
// en un manifiesto —«esto y lo que venga»— y se lee mejor que `>=` cuando hay veinte
|
|
68
|
+
// líneas seguidas (dueño, 2026-09-04).
|
|
69
|
+
if (r.endsWith('+')) {
|
|
70
|
+
try { return compareVersions(version, r.slice(0, -1).trim()) >= 0 } catch (_) { return false }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const guion = r.split(' - ')
|
|
74
|
+
if (guion.length === 2) {
|
|
75
|
+
const desde = guion[0].trim()
|
|
76
|
+
const hasta = guion[1].trim()
|
|
77
|
+
try {
|
|
78
|
+
if (compareVersions(version, desde) < 0) return false
|
|
79
|
+
// El extremo de arriba también puede quedar abierto: «0.100.0 - *».
|
|
80
|
+
return hasta === '*' ? true : compareVersions(version, hasta) <= 0
|
|
81
|
+
} catch (_) { return false }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const [signo, cumple] of OPS) {
|
|
85
|
+
if (r.startsWith(signo)) {
|
|
86
|
+
try { return cumple(compareVersions(version, r.slice(signo.length).trim())) } catch (_) { return false }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try { return compareVersions(version, r) === 0 } catch (_) { return false }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default { parseVersion, compareVersions, satisfies }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/opaque@0.1.0 (dotrino-opaque/{
|
|
1
|
+
Copia vendorizada de @dotrino/opaque@0.1.0 (dotrino-opaque/{build/opaque,build/wasm-bytes,src/index}.js).
|
|
2
2
|
NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
|
|
3
3
|
src/index.js importa ../build/{opaque,wasm-bytes}.js, así que la copia CONSERVA
|
|
4
4
|
esas dos carpetas: aplanarla rompería el import relativo.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
Copia vendorizada de @dotrino/passmanager@0.17.0 (dotrino-passmanager/lib/src/{crypto,fields,generate,import,index,match,model,pubkey,sealed/convert,sealed/device,sealed/entry,sealed/index,sealed/keys,sealed/recovery,sealed/store,session-cache,totp,transport/protocol,transport/proxy,transport/sealed,vault/approval,vault/errors,vault/guard,vault/interface,vault/local,vault/remote,vault/responder,vault/sealed-local,vault/sealed-responder,vault/sealed,webauthn}.js).
|
|
2
|
+
NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
|
|
3
|
+
El sobre de las contraseñas, el mismo que usan el binario y la extensión. Importa @dotrino/identity/content.
|