@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,183 @@
1
+ // Campos sueltos para rellenar formularios: correo, teléfono, dirección, cédula, lo
2
+ // que sea. Son del usuario y no de un sitio concreto.
3
+ //
4
+ // Dos ideas, y las dos son deliberadamente simples:
5
+ //
6
+ // 1. **Los campos son libres.** Una lista de `{ label, value, kind }`, no un formulario
7
+ // fijo. Cada quien guarda lo que necesita rellenar, y puede tener varios juegos —
8
+ // la dirección de casa y la del trabajo son dos entradas, no dos campos.
9
+ //
10
+ // 2. **Cross-domain es no tener sitios**, no un tipo aparte. Una entrada sin `sites`
11
+ // sirve en cualquier parte; con `sites`, solo ahí. Eso vale para cualquier tipo de
12
+ // entrada, y así no hay dos reglas de emparejamiento que mantener.
13
+ //
14
+ // `kind` es opcional y solo sirve para el relleno automático: dice QUÉ ES el dato
15
+ // (un correo, un teléfono), para poder ponerlo en el hueco que le corresponde. Sin
16
+ // `kind`, el campo se guarda y se copia igual — solo no se rellena solo.
17
+
18
+ /** Clases de dato que el relleno automático sabe colocar. */
19
+ export const KINDS = [
20
+ 'email', 'tel', 'given-name', 'family-name', 'full-name',
21
+ 'street-address', 'city', 'region', 'postal-code', 'country',
22
+ 'organization', 'birthday', 'id-number', 'other',
23
+ ]
24
+
25
+ /** Tokens de `autocomplete` del estándar HTML. Cuando el sitio los declara, mandan. */
26
+ export const AUTOCOMPLETE_BY_KIND = {
27
+ email: ['email'],
28
+ tel: ['tel', 'tel-national', 'tel-local'],
29
+ 'given-name': ['given-name'],
30
+ 'family-name': ['family-name'],
31
+ 'full-name': ['name'],
32
+ 'street-address': ['street-address', 'address-line1'],
33
+ city: ['address-level2'],
34
+ region: ['address-level1'],
35
+ 'postal-code': ['postal-code'],
36
+ country: ['country', 'country-name'],
37
+ organization: ['organization'],
38
+ birthday: ['bday'],
39
+ }
40
+
41
+ /** Pistas por nombre/id/etiqueta, para los sitios que no declaran `autocomplete`. */
42
+ export const HINTS_BY_KIND = {
43
+ email: ['email', 'correo', 'e-mail', 'mail'],
44
+ tel: ['phone', 'telefono', 'teléfono', 'celular', 'movil', 'móvil', 'mobile', 'tel'],
45
+ 'given-name': ['firstname', 'first-name', 'first_name', 'givenname', 'nombre', 'nombres', 'fname'],
46
+ 'family-name': ['lastname', 'last-name', 'last_name', 'surname', 'apellido', 'apellidos', 'lname'],
47
+ 'full-name': ['fullname', 'full-name', 'nombrecompleto'],
48
+ 'street-address': ['address', 'direccion', 'dirección', 'street', 'calle', 'domicilio'],
49
+ city: ['city', 'ciudad', 'localidad'],
50
+ region: ['state', 'province', 'provincia', 'region', 'estado'],
51
+ 'postal-code': ['zip', 'zipcode', 'postal', 'postcode'],
52
+ country: ['country', 'pais', 'país'],
53
+ organization: ['company', 'empresa', 'organization', 'organizacion'],
54
+ 'id-number': ['cedula', 'cédula', 'dni', 'nif', 'rut', 'curp', 'documento'],
55
+ }
56
+
57
+ /**
58
+ * Normaliza la lista de campos de una entrada.
59
+ *
60
+ * `private` marca lo que **solo sale de la bóveda con confirmación** (dueño,
61
+ * 2026-08-28). Es la distinción que ya existía entre lo público y lo privado de un
62
+ * registro, ahora dicha campo a campo: el teléfono que rellenas en veinte sitios no
63
+ * pide lo mismo que el número de tu documento.
64
+ */
65
+ export function normalizeFields (fields) {
66
+ if (!Array.isArray(fields)) return []
67
+ return fields
68
+ .filter(f => f && (f.label || f.value))
69
+ .map(f => ({
70
+ label: String(f.label || '').trim(),
71
+ value: String(f.value ?? ''),
72
+ kind: KINDS.includes(f.kind) ? f.kind : undefined,
73
+ ...(f.private ? { private: true } : {}),
74
+ }))
75
+ }
76
+
77
+ /** ¿Esta entrada sirve en cualquier sitio? Sin sitios, sí. */
78
+ export function isCrossDomain (entry) {
79
+ return !entry?.sites?.length
80
+ }
81
+
82
+ /** El primer campo de esa clase. */
83
+ export function fieldOfKind (entry, kind) {
84
+ return (entry?.fields || []).find(f => f.kind === kind) || null
85
+ }
86
+
87
+ /** Un valor por etiqueta, para cuando se sabe qué se busca. */
88
+ export function fieldValue (entry, label) {
89
+ const f = (entry?.fields || []).find(x => x.label === label)
90
+ return f ? f.value : ''
91
+ }
92
+
93
+ /**
94
+ * CÓMO SE LLAMA un campo para decir si dos son «el mismo».
95
+ *
96
+ * La clase si se reconoce, y si no la etiqueta que le puso el sitio. Vive aquí porque la
97
+ * usan las dos puntas —la página, al mirar un formulario, y la bóveda, al decir qué
98
+ * lleva una entrada— y dos ideas distintas de qué es el mismo campo serían un campo
99
+ * duplicado en cada guardado. Su gemela para el DOM está en `extension/src/detect.js`,
100
+ * que corre donde no llega esta librería; `offers.test.mjs` comprueba que dicen lo mismo.
101
+ */
102
+ export function fieldKey (f = {}) {
103
+ if (f.kind) return f.kind
104
+ const l = String(f.label || '').trim()
105
+ return l ? `label:${l}` : 'other'
106
+ }
107
+
108
+ /**
109
+ * LO QUE NO SALE SIN AUTORIZACIÓN, pase lo que pase.
110
+ *
111
+ * La contraseña, el código de dos pasos, las notas y la llave de una passkey son privados
112
+ * por lo que son, no por una marca: nadie los guarda para enseñarlos. El resto de campos
113
+ * lo decide su marca `private` (§4.2), y el usuario es quien la pone al guardar.
114
+ *
115
+ * El **usuario** no está aquí a propósito: es el nombre visible de la entrada (§5), ya
116
+ * viaja en la vista pública, y pedir permiso para escribirlo en el formulario donde lo
117
+ * acabas de teclear no protege nada.
118
+ */
119
+ export const ALWAYS_PRIVATE = ['secret', 'totp', 'notes', 'webauthn']
120
+
121
+ /** Las claves privadas de una entrada abierta: las de arriba, más las marcadas. */
122
+ export function privateKeysOf (open = {}) {
123
+ const out = new Set()
124
+ for (const k of ALWAYS_PRIVATE) if (open[k]) out.add(k)
125
+ const campos = (() => {
126
+ if (Array.isArray(open.fields)) return open.fields
127
+ try { return JSON.parse(open.fields || '[]') } catch { return [] }
128
+ })()
129
+ for (const f of campos) if (f?.private && f?.value) out.add(fieldKey(f))
130
+ return out
131
+ }
132
+
133
+ /**
134
+ * TODOS los campos de una entrada abierta, como pares `{ key, value }`.
135
+ *
136
+ * Es la lista canónica de «qué lleva dentro»: el usuario, la contraseña, el código de dos
137
+ * pasos y cada campo libre, **sin distinguir público de privado**. Esa distinción decide
138
+ * qué se ENSEÑA (§4.2), no qué existe, y mezclar las dos preguntas fue lo que hizo falta
139
+ * separar el 2026-08-29.
140
+ */
141
+ export function entryFieldValues (open = {}) {
142
+ const out = []
143
+ const add = (key, value) => {
144
+ if (!value) return
145
+ if (out.some(f => f.key === key)) return
146
+ out.push({ key, value: String(value) })
147
+ }
148
+ add('username', open.username)
149
+ add('secret', open.secret)
150
+ add('totp', open.totp)
151
+ const campos = (() => {
152
+ if (Array.isArray(open.fields)) return open.fields
153
+ try { return JSON.parse(open.fields || '[]') } catch { return [] }
154
+ })()
155
+ for (const f of campos) if (f?.value) add(fieldKey(f), f.value)
156
+ return out
157
+ }
158
+
159
+ /**
160
+ * LO MISMO, MÁS LAS NOTAS. Solo para los resúmenes (§4.0.2).
161
+ *
162
+ * Las notas no están en `entryFieldValues` a propósito: eso es «qué se puede rellenar en
163
+ * un formulario», y una nota no se rellena en ninguna parte. Pero sí se **edita** en el
164
+ * gestor, y para decir ahí si lo escrito cambia algo hace falta su resumen como el de
165
+ * cualquier otro campo. Un método de comparación, no dos (dueño, 2026-08-29).
166
+ */
167
+ export function entryDigestValues (open = {}) {
168
+ const out = entryFieldValues(open)
169
+ if (open.notes && !out.some(f => f.key === 'notes')) out.push({ key: 'notes', value: String(open.notes) })
170
+ return out
171
+ }
172
+
173
+ /**
174
+ * QUÉ CAMPOS lleva una entrada abierta, por su nombre y sin un solo valor.
175
+ *
176
+ * Es lo que permite ofrecer «rellenar el número de socio» sin abrir nada: lo calcula
177
+ * quien tiene la CEK y viaja en la vista pública (§4.0.2). Los NOMBRES no son los
178
+ * valores — de la misma familia que `sites`, que ya va en claro para poder emparejar con
179
+ * la página, y bastante menos que ellos.
180
+ */
181
+ export function entryFieldKeys (open = {}) {
182
+ return entryFieldValues(open).map(f => f.key)
183
+ }
@@ -0,0 +1,74 @@
1
+ // Generador de contraseñas.
2
+ //
3
+ // Un gestor que no genera contraseñas obliga a inventárselas, y ahí es donde la gente
4
+ // repite la de siempre. Es parte del producto, no un extra.
5
+ //
6
+ // Toda la aleatoriedad sale de `crypto.getRandomValues`, y la elección de carácter se
7
+ // hace SIN sesgo: `% alphabet.length` reparte de más los primeros caracteres cuando el
8
+ // alphabet no divide a 256, y en un generador de contraseñas eso es exactamente lo que
9
+ // no se quiere. Se descartan los valores que caen fuera del último bloque completo.
10
+
11
+ const MINUS = 'abcdefghijkmnopqrstuvwxyz' // sin la ele
12
+ const MAYUS = 'ABCDEFGHJKLMNPQRSTUVWXYZ' // sin la I ni la O
13
+ const DIGITOS = '23456789' // sin el cero ni el uno
14
+ const SIMBOLOS = '!#$%&*+-=?@_'
15
+ const AMBIGUOS = 'lIO01'
16
+
17
+ /** Un entero en [0, max) sin sesgo. */
18
+ function randomBelow (max) {
19
+ const limite = Math.floor(256 / max) * max
20
+ const b = new Uint8Array(1)
21
+ for (;;) {
22
+ globalThis.crypto.getRandomValues(b)
23
+ if (b[0] < limite) return b[0] % max
24
+ }
25
+ }
26
+
27
+ function pick (alphabet) {
28
+ return alphabet[randomBelow(alphabet.length)]
29
+ }
30
+
31
+ /** Baraja en su sitio (Fisher-Yates), para que los obligatorios no queden al principio. */
32
+ function shuffle (arr) {
33
+ for (let i = arr.length - 1; i > 0; i--) {
34
+ const j = randomBelow(i + 1)
35
+ ;[arr[i], arr[j]] = [arr[j], arr[i]]
36
+ }
37
+ return arr
38
+ }
39
+
40
+ /**
41
+ * @param {object} opts
42
+ * `length` size (por defecto 20)
43
+ * `upper`/`digits`/`symbols` incluir mayúsculas, números, símbolos
44
+ * `ambiguous` incluir los caracteres que se confunden al leer (l, I, O, 0, 1)
45
+ */
46
+ export function generatePassword ({
47
+ length = 20, upper = true, digits = true, symbols = true, ambiguous = false,
48
+ } = {}) {
49
+ const groups = [ambiguous ? MINUS + 'l' : MINUS]
50
+ if (upper) groups.push(ambiguous ? MAYUS + 'IO' : MAYUS)
51
+ if (digits) groups.push(ambiguous ? DIGITOS + '01' : DIGITOS)
52
+ if (symbols) groups.push(SIMBOLOS)
53
+
54
+ const alphabet = groups.join('')
55
+ const min = Math.max(groups.length, 4)
56
+ const size = Math.max(min, Math.min(256, Math.floor(length) || 20))
57
+
58
+ // Uno de cada grupo pedido, para que «con números» signifique que HAY un número y no
59
+ // que podría haberlo. El resto, del alphabet entero.
60
+ const chars = groups.map(g => pick(g))
61
+ while (chars.length < size) chars.push(pick(alphabet))
62
+ return shuffle(chars).join('')
63
+ }
64
+
65
+ /** Frase de varias palabras, más fácil de dictar o teclear a mano. */
66
+ export function generatePassphrase (words, { count = 4, separator = '-' } = {}) {
67
+ if (!Array.isArray(words) || words.length < 16) {
68
+ throw new Error('generatePassphrase: hace falta una lista de palabras')
69
+ }
70
+ return Array.from({ length: Math.max(2, count) }, () => words[randomBelow(words.length)])
71
+ .join(separator)
72
+ }
73
+
74
+ export { AMBIGUOS }
@@ -0,0 +1,156 @@
1
+ // Importar desde otros gestores. Sin esto nadie migra, por bueno que sea el resto
2
+ // (DISENO §8, paso 1).
3
+ //
4
+ // Devuelve entradas EN CLARO, listas para `sealEntry`. Esta función no cifra ni
5
+ // guarda: quien importa es quien tiene la CEK, y es él quien decide.
6
+
7
+ import { normalizeSites } from './model.js'
8
+ import { hostOf } from './match.js'
9
+
10
+ /** CSV con comillas, comas y saltos de línea dentro de campo (RFC 4180). */
11
+ export function parseCsv (text) {
12
+ const rows = []
13
+ let row = []
14
+ let field = ''
15
+ let quoted = false
16
+ const s = String(text || '').replace(/^/, '')
17
+
18
+ for (let i = 0; i < s.length; i++) {
19
+ const c = s[i]
20
+ if (quoted) {
21
+ if (c === '"') {
22
+ if (s[i + 1] === '"') { field += '"'; i++ } else { quoted = false }
23
+ } else field += c
24
+ continue
25
+ }
26
+ if (c === '"') { quoted = true; continue }
27
+ if (c === ',') { row.push(field); field = ''; continue }
28
+ if (c === '\r') continue
29
+ if (c === '\n') { row.push(field); rows.push(row); row = []; field = ''; continue }
30
+ field += c
31
+ }
32
+ if (field !== '' || row.length) { row.push(field); rows.push(row) }
33
+ return rows.filter(r => r.some(v => v !== ''))
34
+ }
35
+
36
+ function csvObjects (text) {
37
+ const rows = parseCsv(text)
38
+ if (!rows.length) return []
39
+ const head = rows[0].map(h => h.trim().toLowerCase())
40
+ return rows.slice(1).map(r => {
41
+ const o = {}
42
+ head.forEach((h, i) => { o[h] = r[i] ?? '' })
43
+ return o
44
+ })
45
+ }
46
+
47
+ function sitesFrom (...urls) {
48
+ const out = []
49
+ for (const u of urls.flat()) {
50
+ const h = hostOf(u)
51
+ if (h) out.push(h)
52
+ }
53
+ return normalizeSites(out)
54
+ }
55
+
56
+ function entryFrom ({ title, sites, username, secret, totp, notes }) {
57
+ return {
58
+ type: 'login',
59
+ title: String(title || '').trim() || (sites[0] || 'sin nombre'),
60
+ sites,
61
+ username: String(username || ''),
62
+ secret: String(secret || ''),
63
+ totp: String(totp || ''),
64
+ notes: String(notes || ''),
65
+ }
66
+ }
67
+
68
+ /** Chrome / Edge: `name,url,username,password,note`. */
69
+ export function fromChromeCsv (text) {
70
+ return csvObjects(text)
71
+ .map(o => entryFrom({
72
+ title: o.name,
73
+ sites: sitesFrom(o.url),
74
+ username: o.username,
75
+ secret: o.password,
76
+ notes: o.note || o.notes,
77
+ }))
78
+ .filter(e => e.secret || e.username)
79
+ }
80
+
81
+ /** 1Password: `Title,Url,Username,Password,OTPAuth,Favorite,Archived,Tags,Notes`. */
82
+ export function from1PasswordCsv (text) {
83
+ return csvObjects(text)
84
+ .map(o => entryFrom({
85
+ title: o.title,
86
+ sites: sitesFrom(o.url, o.urls),
87
+ username: o.username,
88
+ secret: o.password,
89
+ totp: o.otpauth,
90
+ notes: o.notes,
91
+ }))
92
+ .filter(e => e.secret || e.username)
93
+ }
94
+
95
+ /** Bitwarden CSV (`login_uri`, `login_username`, `login_password`, `login_totp`). */
96
+ export function fromBitwardenCsv (text) {
97
+ return csvObjects(text)
98
+ .filter(o => !o.type || o.type === 'login')
99
+ .map(o => entryFrom({
100
+ title: o.name,
101
+ sites: sitesFrom((o.login_uri || '').split(',')),
102
+ username: o.login_username,
103
+ secret: o.login_password,
104
+ totp: o.login_totp,
105
+ notes: o.notes,
106
+ }))
107
+ .filter(e => e.secret || e.username)
108
+ }
109
+
110
+ /** Bitwarden JSON (export sin cifrar). */
111
+ export function fromBitwardenJson (json) {
112
+ const data = typeof json === 'string' ? JSON.parse(json) : json
113
+ if (data?.encrypted) {
114
+ const e = new Error('el export de Bitwarden está cifrado: expórtalo sin cifrar')
115
+ e.code = 'encrypted-export'
116
+ throw e
117
+ }
118
+ return (data?.items || [])
119
+ .filter(it => it.type === 1 && it.login)
120
+ .map(it => entryFrom({
121
+ title: it.name,
122
+ sites: sitesFrom((it.login.uris || []).map(u => u.uri)),
123
+ username: it.login.username,
124
+ secret: it.login.password,
125
+ totp: it.login.totp,
126
+ notes: it.notes,
127
+ }))
128
+ }
129
+
130
+ /**
131
+ * Detecta el formato por su cabecera y lo importa. Es lo que consume la UI: el
132
+ * usuario suelta el archivo y no tiene por qué saber qué formato es.
133
+ *
134
+ * `1pux` (el export nativo de 1Password) NO entra: es un zip. Se descomprime en la
135
+ * UI y se le pasa aquí el JSON de dentro.
136
+ */
137
+ export function importAuto (text) {
138
+ const t = String(text || '').trim()
139
+ if (!t) return { format: 'vacío', entries: [] }
140
+
141
+ if (t.startsWith('{')) return { format: 'bitwarden-json', entries: fromBitwardenJson(t) }
142
+
143
+ const head = (parseCsv(t)[0] || []).map(h => h.trim().toLowerCase())
144
+ if (head.includes('login_password') || head.includes('login_username')) {
145
+ return { format: 'bitwarden-csv', entries: fromBitwardenCsv(t) }
146
+ }
147
+ if (head.includes('otpauth') || (head.includes('title') && head.includes('url'))) {
148
+ return { format: '1password-csv', entries: from1PasswordCsv(t) }
149
+ }
150
+ if (head.includes('name') && head.includes('url') && head.includes('password')) {
151
+ return { format: 'chrome-csv', entries: fromChromeCsv(t) }
152
+ }
153
+ const e = new Error('no reconozco el formato del archivo')
154
+ e.code = 'unknown-format'
155
+ throw e
156
+ }
@@ -0,0 +1,27 @@
1
+ export * from './crypto.js'
2
+ export * from './model.js'
3
+ export * from './match.js'
4
+ export * from './fields.js'
5
+ export * from './generate.js'
6
+ // Los importadores son parte de la API: sin esto una app tenía que ir al subpath, y
7
+ // eso se descubre cuando ya está escrito el import.
8
+ export * from './import.js'
9
+ export * from './pubkey.js'
10
+ export * from './totp.js'
11
+ export { LocalVault } from './vault/local.js'
12
+ // Las contraseñas SELLADAS por aparato viven en `./sealed/` y se importan por su subpath
13
+ // (`@dotrino/passmanager/sealed`), que es lo que arrastra la cripto de los sobres. Aquí solo
14
+ // las dos puntas del protocolo, que es lo que cablea cada bóveda y cada aparato.
15
+ export { SealedVault } from './vault/sealed.js'
16
+ export { SealedResponder } from './vault/sealed-responder.js'
17
+ export { SealedLocalVault } from './vault/sealed-local.js'
18
+ export { SealedStore, SealedError } from './sealed/store.js'
19
+ export { RemoteVault } from './vault/remote.js'
20
+ export { VaultResponder } from './vault/responder.js'
21
+ export { GuardedVault } from './vault/guard.js'
22
+ export { ApprovalGate } from './vault/approval.js'
23
+ export { SessionCache } from './session-cache.js'
24
+ export { ProxyTransport } from './transport/proxy.js'
25
+ export * from './transport/protocol.js'
26
+ export * from './transport/sealed.js'
27
+ export { VaultError, CODES } from './vault/errors.js'
@@ -0,0 +1,137 @@
1
+ // Emparejamiento de una entrada con el sitio que el usuario tiene delante.
2
+ //
3
+ // Es la pieza de la que depende la seguridad del autocompletado: emparejar de más
4
+ // entrega una credencial al sitio equivocado. Por eso todo se compara por ETIQUETAS
5
+ // de dominio, nunca por sufijo de cadena — `evil-salesforce.com` termina en
6
+ // `salesforce.com` como texto y no tiene nada que ver con él.
7
+
8
+ // Sufijos públicos de dos etiquetas más comunes. NO es la Public Suffix List entera
9
+ // (son miles de reglas y varios cientos de KB); es la aproximación que cubre el caso
10
+ // real. Deuda anotada: cargar la PSL de verdad cuando el autocompletado esté maduro.
11
+ // Mientras tanto un sufijo desconocido solo hace el emparejamiento MÁS estricto, que
12
+ // es el lado seguro por el que fallar.
13
+ const TWO_LABEL_SUFFIXES = new Set([
14
+ 'co.uk', 'org.uk', 'ac.uk', 'gov.uk', 'me.uk', 'net.uk', 'sch.uk',
15
+ 'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'id.au',
16
+ 'com.br', 'net.br', 'org.br', 'gov.br',
17
+ 'com.ec', 'net.ec', 'org.ec', 'gob.ec', 'edu.ec', 'fin.ec', 'med.ec',
18
+ 'com.ar', 'net.ar', 'org.ar', 'gob.ar', 'edu.ar',
19
+ 'com.mx', 'net.mx', 'org.mx', 'gob.mx', 'edu.mx',
20
+ 'com.co', 'net.co', 'org.co', 'gov.co', 'edu.co',
21
+ 'co.jp', 'ne.jp', 'or.jp', 'ac.jp', 'go.jp',
22
+ 'com.cn', 'net.cn', 'org.cn', 'gov.cn', 'edu.cn',
23
+ 'co.nz', 'net.nz', 'org.nz', 'govt.nz',
24
+ 'co.za', 'org.za', 'net.za',
25
+ 'com.tr', 'com.tw', 'com.hk', 'com.sg', 'com.my', 'com.pe', 'com.uy',
26
+ 'com.ve', 'com.py', 'com.bo', 'com.do', 'com.gt', 'com.pa', 'com.pl',
27
+ 'com.es', 'com.pt', 'com.ua', 'com.ru', 'com.vn', 'com.ph',
28
+ ])
29
+
30
+ /** Extrae el host de una URL o de algo que ya es un host. Devuelve '' si no vale. */
31
+ export function hostOf (input) {
32
+ if (typeof input !== 'string') return ''
33
+ let s = input.trim().toLowerCase()
34
+ if (!s) return ''
35
+ if (!s.includes('://')) s = 'https://' + s
36
+ let u
37
+ try { u = new URL(s) } catch { return '' }
38
+ // Solo esquemas web: un `javascript:` o un `file:` no tienen sitio con el que emparejar.
39
+ if (u.protocol !== 'https:' && u.protocol !== 'http:') return ''
40
+ return u.hostname.replace(/\.$/, '')
41
+ }
42
+
43
+ /** Dominio registrable (eTLD+1 aproximado). Para una IP o un host raro, el host tal cual. */
44
+ export function registrableDomain (host) {
45
+ if (!host) return ''
46
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(host) || host.includes(':')) return host
47
+ const parts = host.split('.')
48
+ if (parts.length <= 2) return host
49
+ const lastTwo = parts.slice(-2).join('.')
50
+ if (TWO_LABEL_SUFFIXES.has(lastTwo)) return parts.slice(-3).join('.')
51
+ return lastTwo
52
+ }
53
+
54
+ /** ¿`host` es igual a `base` o un subdominio suyo? Compara por etiquetas. */
55
+ export function isSameOrSubdomainOf (host, base) {
56
+ if (!host || !base) return false
57
+ if (host === base) return true
58
+ return host.endsWith('.' + base)
59
+ }
60
+
61
+ export const MATCH = {
62
+ EXACT: 3,
63
+ SUBDOMAIN: 2,
64
+ /** Sin sitios: sirve en cualquier parte, y por eso va la última. */
65
+ ANY: 0.5,
66
+ NONE: 0,
67
+ }
68
+
69
+ // `DOMAIN` (mismo dominio registrable) EXISTIÓ y se quitó el 2026-08-29, porque hacía
70
+ // justo lo que un gestor de contraseñas no puede hacer: con `pass.dotrino.com` guardado,
71
+ // `r.dotrino.com` recibía sus credenciales — dos servicios distintos que solo comparten
72
+ // el dominio de arriba (lo vio el dueño: *«en r.dotrino.com me salen records de
73
+ // pass.dotrino.com»*).
74
+ //
75
+ // Se puso para el caso «el subdominio cambió y la clave es la misma», pero para eso no
76
+ // hacía falta: guardar el dominio a secas (`empresa.com`) YA cubre todos sus subdominios
77
+ // por la vía de SUBDOMAIN, y traerse la cuenta de otro dominio ya tiene su camino
78
+ // explícito —buscarla (§4.1)—, que además lo decide una persona en vez de la máquina.
79
+ //
80
+ // Lo único que aportaba era el sentido contrario: un subdominio GUARDADO emparejando con
81
+ // sus hermanos y con el dominio padre. Y ese sentido es exactamente la fuga.
82
+
83
+ /**
84
+ * Compara UN patrón de `sites` con el host actual.
85
+ * Patrones admitidos: `ejemplo.com`, `*.ejemplo.com`, o una URL entera.
86
+ *
87
+ * Un patrón cubre **el host y lo que cuelga de él**, nunca hacia arriba ni hacia los
88
+ * lados: `empresa.com` vale en `login.empresa.com`, pero `login.empresa.com` NO vale en
89
+ * `otra.empresa.com`. Por eso el comodín casi nunca hace falta — un dominio a secas ya
90
+ * trae sus subdominios— y por eso `*.x` y `x` cubren hoy lo mismo.
91
+ */
92
+ export function matchSite (pattern, currentHost) {
93
+ if (!currentHost) return MATCH.NONE
94
+ let p = String(pattern || '').trim().toLowerCase()
95
+ if (!p) return MATCH.NONE
96
+
97
+ let wildcard = false
98
+ if (p.startsWith('*.')) { wildcard = true; p = p.slice(2) }
99
+
100
+ const base = hostOf(p)
101
+ if (!base) return MATCH.NONE
102
+
103
+ if (wildcard) return isSameOrSubdomainOf(currentHost, base) ? MATCH.SUBDOMAIN : MATCH.NONE
104
+ if (currentHost === base) return MATCH.EXACT
105
+ if (isSameOrSubdomainOf(currentHost, base)) return MATCH.SUBDOMAIN
106
+ // Y nada más. Un hermano NO empareja: ver la nota de `MATCH`.
107
+ return MATCH.NONE
108
+ }
109
+
110
+ /**
111
+ * Mejor nivel de emparejamiento de una entrada con la URL actual.
112
+ *
113
+ * **Una entrada SIN sitios sirve en cualquier parte.** Es como se dice «esto no es de
114
+ * un dominio»: el correo o la cédula valen en todas partes, la contraseña del banco
115
+ * no. No hace falta un tipo aparte para eso, y así hay una sola regla que mantener.
116
+ * Empareja siempre por debajo de lo que sí es de este sitio.
117
+ */
118
+ export function matchEntry (entry, url) {
119
+ const host = hostOf(url)
120
+ if (!host) return MATCH.NONE
121
+ if (!entry?.sites?.length) return MATCH.ANY
122
+ let best = MATCH.NONE
123
+ for (const s of entry?.sites || []) {
124
+ const m = matchSite(s, host)
125
+ if (m > best) best = m
126
+ if (best === MATCH.EXACT) break
127
+ }
128
+ return best
129
+ }
130
+
131
+ /** Entradas que sirven para esta URL, de mejor a peor emparejamiento. */
132
+ export function findForUrl (entries, url) {
133
+ return (entries || [])
134
+ .map(e => ({ entry: e, match: matchEntry(e, url) }))
135
+ .filter(x => x.match > MATCH.NONE)
136
+ .sort((a, b) => b.match - a.match || (b.entry.updatedAt || 0) - (a.entry.updatedAt || 0))
137
+ }