@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,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/{src/index,build/opaque,build/wasm-bytes}.js).
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.
@@ -0,0 +1,167 @@
1
+ // Cifrado de las entradas de la bóveda.
2
+ //
3
+ // Una CEK (AES-GCM 256) por bóveda, con la que se cifra cada entrada por separado.
4
+ // Quién tiene la CEK es lo que define la confianza de cada aparato (DISENO §3): al
5
+ // vault y a la app nativa se les envuelve; a la extensión, no.
6
+ //
7
+ // El envoltorio de la CEK a cada miembro NO se hace aquí: eso es `wrapForMember` de
8
+ // @dotrino/identity/content, que ya está escrito y probado. Este módulo es solo el
9
+ // tramo simétrico.
10
+
11
+ const enc = new TextEncoder()
12
+ const dec = new TextDecoder()
13
+
14
+ /** WebCrypto del entorno (navegador, service worker o Node ≥ 20). */
15
+ function subtle () {
16
+ const c = globalThis.crypto
17
+ if (!c?.subtle) throw new Error('webcrypto unavailable')
18
+ return c.subtle
19
+ }
20
+
21
+ function randomBytes (n) {
22
+ const b = new Uint8Array(n)
23
+ globalThis.crypto.getRandomValues(b)
24
+ return b
25
+ }
26
+
27
+ export function toBase64 (bytes) {
28
+ let s = ''
29
+ for (const b of bytes) s += String.fromCharCode(b)
30
+ return btoa(s)
31
+ }
32
+
33
+ export function fromBase64 (b64) {
34
+ const s = atob(b64)
35
+ const out = new Uint8Array(s.length)
36
+ for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i)
37
+ return out
38
+ }
39
+
40
+ /** Genera una CEK nueva. Extraíble: hay que poder envolverla para cada aparato. */
41
+ export async function makeVaultKey () {
42
+ return subtle().generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'])
43
+ }
44
+
45
+ export async function exportVaultKey (key) {
46
+ return new Uint8Array(await subtle().exportKey('raw', key))
47
+ }
48
+
49
+ export async function importVaultKey (raw) {
50
+ return subtle().importKey('raw', raw, { name: 'AES-GCM' }, true, ['encrypt', 'decrypt'])
51
+ }
52
+
53
+ /**
54
+ * Cifra un valor con la CEK. Devuelve `{ iv, ct }` en base64, para que una entrada
55
+ * cifrada siga siendo JSON serializable tal cual.
56
+ *
57
+ * `aad` ata el criptograma a su sitio: se pasa el id de la entrada y el campo, así
58
+ * que un criptograma movido de una entrada a otra (o de `secret` a `totp`) no abre.
59
+ */
60
+ export async function sealValue (key, plaintext, aad) {
61
+ const iv = randomBytes(12)
62
+ const params = { name: 'AES-GCM', iv }
63
+ if (aad) params.additionalData = enc.encode(aad)
64
+ const ct = await subtle().encrypt(params, key, enc.encode(plaintext))
65
+ return { iv: toBase64(iv), ct: toBase64(new Uint8Array(ct)) }
66
+ }
67
+
68
+ export async function openValue (key, sealed, aad) {
69
+ if (!sealed || typeof sealed.iv !== 'string' || typeof sealed.ct !== 'string') {
70
+ throw new Error('sealed value malformed')
71
+ }
72
+ const params = { name: 'AES-GCM', iv: fromBase64(sealed.iv) }
73
+ if (aad) params.additionalData = enc.encode(aad)
74
+ const pt = await subtle().decrypt(params, key, fromBase64(sealed.ct))
75
+ return dec.decode(pt)
76
+ }
77
+
78
+ /** Etiqueta AAD de un campo. Un solo sitio para que sellar y abrir no se desalineen. */
79
+ export function fieldTag (entryId, field) {
80
+ return `dotrino-passmanager/v1/${entryId}/${field}`
81
+ }
82
+
83
+ // --- Contraseña maestra ------------------------------------------------------
84
+ //
85
+ // Solo para el caso en que la bóveda vive en el propio aparato y no hay nadie más
86
+ // que la custodie (la extensión antes del paso 2, DISENO §8). Cuando responde el
87
+ // vault o el teléfono, la CEK llega envuelta y esto no se usa.
88
+
89
+ const KDF_ITERATIONS = 600_000 // recomendación OWASP para PBKDF2-SHA256
90
+
91
+ export function makeSalt () {
92
+ return randomBytes(16)
93
+ }
94
+
95
+ /**
96
+ * Deriva la CEK de una contraseña maestra.
97
+ *
98
+ * `extractable` existe por MV3: el service worker de la extensión se duerme, y un
99
+ * `CryptoKey` no sobrevive a `chrome.storage.session` (que serializa a JSON, no
100
+ * clona). Para no pedir la contraseña cada pocos minutos hay que poder exportar la
101
+ * clave y guardarla en memoria de sesión. Es un compromiso consciente y acotado a la
102
+ * extensión: donde no haga falta, se deriva no extraíble.
103
+ */
104
+ export async function deriveKeyFromPassword (password, salt, iterations = KDF_ITERATIONS, extractable = false) {
105
+ const material = await subtle().importKey(
106
+ 'raw', enc.encode(String(password)), 'PBKDF2', false, ['deriveKey'])
107
+ return subtle().deriveKey(
108
+ { name: 'PBKDF2', salt, iterations, hash: 'SHA-256' },
109
+ material,
110
+ { name: 'AES-GCM', length: 256 },
111
+ extractable,
112
+ ['encrypt', 'decrypt'],
113
+ )
114
+ }
115
+
116
+ /**
117
+ * Verificador de contraseña: un valor sellado con la propia clave derivada. No es un
118
+ * hash de la contraseña — es una prueba de que la clave abre la bóveda, que es lo que
119
+ * de verdad importa.
120
+ */
121
+ export async function makeVerifier (key) {
122
+ return sealValue(key, 'dotrino-passmanager', 'verifier')
123
+ }
124
+
125
+ export async function checkVerifier (key, verifier) {
126
+ try {
127
+ return (await openValue(key, verifier, 'verifier')) === 'dotrino-passmanager'
128
+ } catch {
129
+ return false
130
+ }
131
+ }
132
+
133
+ // --- comparar sin abrir -------------------------------------------------------
134
+ //
135
+ // Para decir «esto que escribiste ya está guardado igual» hace falta comparar con lo
136
+ // guardado, y abrir la entrada es una autorización (DISENO §3.3.2). La bóveda manda
137
+ // entonces un **resumen** de cada campo: quien pregunta hashea lo que tiene delante y
138
+ // compara los resúmenes.
139
+ //
140
+ // **Un solo método para todos los campos**, públicos y privados (dueño, 2026-08-29):
141
+ // dos formas de comparar según qué campo sea son dos comportamientos que se separan y
142
+ // acaban diciendo cosas distintas.
143
+ //
144
+ // El resumen va con un **nonce nuevo en cada respuesta**, así que no es una huella
145
+ // estable: no vale para reconocer la misma contraseña entre dos respuestas, ni para
146
+ // guardar una tabla y consultarla más tarde. Y el nombre del campo entra en el HMAC, así
147
+ // que un resumen tampoco sirve para comparar contra otro campo.
148
+
149
+ /** Un nonce nuevo, para UNA respuesta. No se guarda en ninguna parte. */
150
+ export function makeNonce () {
151
+ return toBase64(randomBytes(16))
152
+ }
153
+
154
+ /**
155
+ * El hasheador de una respuesta. Se importa la llave UNA vez y se hashean todos los
156
+ * campos con ella: importar por campo multiplica el coste de un `find` sin ganar nada.
157
+ */
158
+ export async function fieldHasher (nonce) {
159
+ const key = await subtle().importKey(
160
+ 'raw', fromBase64(nonce), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])
161
+ return async (name, value) => {
162
+ const sig = await subtle().sign('HMAC', key, enc.encode(name + ' ' + value))
163
+ // 128 bits: de sobra para que dos valores distintos no colisionen en la práctica, y
164
+ // la mitad de bytes que mandar el SHA-256 entero.
165
+ return toBase64(new Uint8Array(sig).slice(0, 16))
166
+ }
167
+ }