@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.
- package/package.json +7 -1
- package/src/index.js +9 -1
- package/src/node.js +4 -180
- package/vault/acta.js +5 -1
- package/vault/core.js +95 -15
- package/vault/facade.js +200 -0
- package/vault/index.html +10 -0
- package/vault/machine.js +95 -0
- package/vault/remote.js +54 -9
- package/vault/vault.js +29 -58
- package/vault/vendor/compat/VERSION.txt +3 -0
- package/vault/vendor/compat/advisory.js +141 -0
- package/vault/vendor/compat/index.js +188 -0
- package/vault/vendor/compat/ranges.js +93 -0
- package/vault/vendor/opaque/VERSION.txt +1 -1
- package/vault/vendor/passmanager/VERSION.txt +3 -0
- package/vault/vendor/passmanager/crypto.js +167 -0
- package/vault/vendor/passmanager/fields.js +183 -0
- package/vault/vendor/passmanager/generate.js +74 -0
- package/vault/vendor/passmanager/import.js +156 -0
- package/vault/vendor/passmanager/index.js +27 -0
- package/vault/vendor/passmanager/match.js +137 -0
- package/vault/vendor/passmanager/model.js +332 -0
- package/vault/vendor/passmanager/pubkey.js +33 -0
- package/vault/vendor/passmanager/sealed/convert.js +72 -0
- package/vault/vendor/passmanager/sealed/device.js +211 -0
- package/vault/vendor/passmanager/sealed/entry.js +209 -0
- package/vault/vendor/passmanager/sealed/index.js +20 -0
- package/vault/vendor/passmanager/sealed/keys.js +120 -0
- package/vault/vendor/passmanager/sealed/recovery.js +136 -0
- package/vault/vendor/passmanager/sealed/store.js +508 -0
- package/vault/vendor/passmanager/session-cache.js +60 -0
- package/vault/vendor/passmanager/totp.js +111 -0
- package/vault/vendor/passmanager/transport/protocol.js +83 -0
- package/vault/vendor/passmanager/transport/proxy.js +112 -0
- package/vault/vendor/passmanager/transport/sealed.js +74 -0
- package/vault/vendor/passmanager/vault/approval.js +63 -0
- package/vault/vendor/passmanager/vault/errors.js +34 -0
- package/vault/vendor/passmanager/vault/guard.js +114 -0
- package/vault/vendor/passmanager/vault/interface.js +34 -0
- package/vault/vendor/passmanager/vault/local.js +213 -0
- package/vault/vendor/passmanager/vault/remote.js +85 -0
- package/vault/vendor/passmanager/vault/responder.js +192 -0
- package/vault/vendor/passmanager/vault/sealed-local.js +199 -0
- package/vault/vendor/passmanager/vault/sealed-responder.js +159 -0
- package/vault/vendor/passmanager/vault/sealed.js +362 -0
- package/vault/vendor/passmanager/webauthn.js +221 -0
- package/vault/vendor/proxy-client/VERSION.txt +1 -1
- package/vault/vendor/proxy-client/client.js +36 -0
- package/vault/vendor/roadmap/VERSION.txt +3 -0
- package/vault/vendor/roadmap/index.js +108 -0
- package/vault/vendor/roadmap/manifests.js +797 -0
- package/vault/vendor/store/VERSION.txt +3 -0
- package/vault/vendor/store/core.js +309 -0
- package/vault/vendor/vault/VERSION.txt +4 -1
- package/vault/vendor/vault/admin.js +437 -0
- package/vault/vendor/vault/browser.js +167 -0
- package/vault/vendor/vault/core/approvals.js +204 -0
- package/vault/vendor/vault/core/blocked.js +68 -0
- package/vault/vendor/vault/core/browserDisk.js +109 -0
- package/vault/vendor/vault/core/commKey.js +68 -0
- package/vault/vendor/vault/core/disk.js +65 -0
- package/vault/vendor/vault/core/libVersion.js +8 -0
- package/vault/vendor/vault/core/machines.js +66 -0
- package/vault/vendor/vault/core/notices.js +71 -0
- package/vault/vendor/vault/core/notifiable.js +73 -0
- package/vault/vendor/vault/core/passwords.js +125 -0
- package/vault/vendor/vault/core/sealKey.js +77 -0
- package/vault/vendor/vault/core/sealer.js +172 -0
- package/vault/vendor/vault/core/sealers.js +159 -0
- package/vault/vendor/vault/core/secretsStore.js +1326 -0
- package/vault/vendor/vault/core/sha256.js +69 -0
- package/vault/vendor/vault/core/store.js +68 -0
- package/vault/vendor/vault/core/subacta.js +101 -0
- package/vault/vendor/vault/core/threadStore.js +273 -0
- package/vault/vendor/vault/core/transport.js +270 -0
- package/vault/vendor/vault/core/vault.js +4414 -0
- package/vault/vendor/vault/enroll.js +8 -4
- package/vault/vendor/vault/procContext.js +97 -0
- package/vault/vendor/vault/revocation.js +67 -0
- package/vault/vendor/vault/sealed.js +84 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Passkeys: crear una credencial WebAuthn y firmar con ella.
|
|
2
|
+
//
|
|
3
|
+
// Chrome NO expone API de proveedor de credenciales a las extensiones (Android 14+ e
|
|
4
|
+
// iOS 17+ sí). En el escritorio la única vía es la que usan todos: reemplazar
|
|
5
|
+
// `navigator.credentials.create/get` en el contexto de la página. Como la llave la
|
|
6
|
+
// generamos nosotros y la firma la producimos nosotros, la assertion es válida y el
|
|
7
|
+
// sitio no distingue.
|
|
8
|
+
//
|
|
9
|
+
// Este módulo es la parte que NO toca el DOM: qué se guarda y cómo se firma. El parche
|
|
10
|
+
// vive en la extensión.
|
|
11
|
+
//
|
|
12
|
+
// Referencia: WebAuthn Level 2, §6.3 (authenticatorMakeCredential) y §6.3.3
|
|
13
|
+
// (authenticatorGetAssertion). El formato importa hasta el bit: un `authenticatorData`
|
|
14
|
+
// mal armado lo rechaza el servidor sin decir por qué.
|
|
15
|
+
|
|
16
|
+
const subtle = () => globalThis.crypto.subtle
|
|
17
|
+
const enc = new TextEncoder()
|
|
18
|
+
|
|
19
|
+
export const AAGUID = new Uint8Array(16) // ceros: sin atestación, como todo proveedor de software
|
|
20
|
+
|
|
21
|
+
export function b64urlEncode (bytes) {
|
|
22
|
+
let s = ''
|
|
23
|
+
for (const b of new Uint8Array(bytes)) s += String.fromCharCode(b)
|
|
24
|
+
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function b64urlDecode (str) {
|
|
28
|
+
const b64 = String(str).replace(/-/g, '+').replace(/_/g, '/')
|
|
29
|
+
const bin = atob(b64)
|
|
30
|
+
const out = new Uint8Array(bin.length)
|
|
31
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)
|
|
32
|
+
return out
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function concat (...arrays) {
|
|
36
|
+
const total = arrays.reduce((n, a) => n + a.length, 0)
|
|
37
|
+
const out = new Uint8Array(total)
|
|
38
|
+
let i = 0
|
|
39
|
+
for (const a of arrays) { out.set(a, i); i += a.length }
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function sha256 (bytes) {
|
|
44
|
+
return new Uint8Array(await subtle().digest('SHA-256', bytes))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Banderas del authenticatorData. UP = presencia, UV = verificación, AT = trae credencial. */
|
|
48
|
+
export const FLAGS = { UP: 0x01, UV: 0x04, AT: 0x40 }
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `authenticatorData`: hash del rpId + banderas + contador (+ la credencial al crear).
|
|
52
|
+
* El orden y los tamaños son fijos; aquí no hay margen de interpretación.
|
|
53
|
+
*/
|
|
54
|
+
export async function makeAuthenticatorData ({ rpId, flags, signCount, attested = null }) {
|
|
55
|
+
const rpIdHash = await sha256(enc.encode(rpId))
|
|
56
|
+
const counter = new Uint8Array(4)
|
|
57
|
+
new DataView(counter.buffer).setUint32(0, signCount >>> 0, false) // big-endian
|
|
58
|
+
const header = concat(rpIdHash, new Uint8Array([flags]), counter)
|
|
59
|
+
return attested ? concat(header, attested) : header
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** COSE_Key de una pública P-256 (kty 2, alg -7, crv 1). Es lo que el servidor guarda. */
|
|
63
|
+
export function coseFromJwk (jwk) {
|
|
64
|
+
const x = b64urlDecode(jwk.x)
|
|
65
|
+
const y = b64urlDecode(jwk.y)
|
|
66
|
+
// CBOR a mano: un mapa de 5 entradas con claves enteras. La alternativa era arrastrar
|
|
67
|
+
// una librería de CBOR entera para escribir 77 bytes de forma fija.
|
|
68
|
+
return concat(
|
|
69
|
+
new Uint8Array([0xa5]), // map(5)
|
|
70
|
+
new Uint8Array([0x01, 0x02]), // 1 (kty): 2 (EC2)
|
|
71
|
+
new Uint8Array([0x03, 0x26]), // 3 (alg): -7 (ES256)
|
|
72
|
+
new Uint8Array([0x20, 0x01]), // -1 (crv): 1 (P-256)
|
|
73
|
+
new Uint8Array([0x21, 0x58, 0x20]), x, // -2 (x): bytes(32)
|
|
74
|
+
new Uint8Array([0x22, 0x58, 0x20]), y, // -3 (y): bytes(32)
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** `attestedCredentialData`: aaguid + size + id + la pública en COSE. */
|
|
79
|
+
export function attestedCredentialData (credentialId, coseKey) {
|
|
80
|
+
const size = new Uint8Array(2)
|
|
81
|
+
new DataView(size.buffer).setUint16(0, credentialId.length, false)
|
|
82
|
+
return concat(AAGUID, size, credentialId, coseKey)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Crea una passkey para un sitio. Devuelve lo que se guarda en la bóveda y lo que hay
|
|
87
|
+
* que devolverle a la página.
|
|
88
|
+
*/
|
|
89
|
+
export async function createCredential ({ rpId, userHandle, userName = '', origin, challenge }) {
|
|
90
|
+
const pair = await subtle().generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'])
|
|
91
|
+
const jwkPriv = await subtle().exportKey('jwk', pair.privateKey)
|
|
92
|
+
const jwkPub = await subtle().exportKey('jwk', pair.publicKey)
|
|
93
|
+
|
|
94
|
+
const credentialId = globalThis.crypto.getRandomValues(new Uint8Array(32))
|
|
95
|
+
const attested = attestedCredentialData(credentialId, coseFromJwk(jwkPub))
|
|
96
|
+
const authData = await makeAuthenticatorData({
|
|
97
|
+
rpId,
|
|
98
|
+
// UV va puesto porque el usuario abre su bóveda para esto: hay verificación real.
|
|
99
|
+
flags: FLAGS.UP | FLAGS.UV | FLAGS.AT,
|
|
100
|
+
signCount: 0,
|
|
101
|
+
attested,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const clientDataJSON = enc.encode(JSON.stringify({
|
|
105
|
+
type: 'webauthn.create',
|
|
106
|
+
challenge: typeof challenge === 'string' ? challenge : b64urlEncode(challenge),
|
|
107
|
+
origin,
|
|
108
|
+
crossOrigin: false,
|
|
109
|
+
}))
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
// Lo que se guarda (la privada va cifrada por el modelo, como cualquier secreto).
|
|
113
|
+
entry: {
|
|
114
|
+
credentialId: b64urlEncode(credentialId),
|
|
115
|
+
rpId,
|
|
116
|
+
userHandle: userHandle ? b64urlEncode(userHandle) : '',
|
|
117
|
+
userName,
|
|
118
|
+
privateKey: JSON.stringify(jwkPriv),
|
|
119
|
+
signCount: 0,
|
|
120
|
+
},
|
|
121
|
+
// Lo que se le devuelve a la página, ya en base64url.
|
|
122
|
+
response: {
|
|
123
|
+
id: b64urlEncode(credentialId),
|
|
124
|
+
rawId: b64urlEncode(credentialId),
|
|
125
|
+
type: 'public-key',
|
|
126
|
+
response: {
|
|
127
|
+
clientDataJSON: b64urlEncode(clientDataJSON),
|
|
128
|
+
// `attestationObject` sin atestación: fmt "none", attStmt vacío.
|
|
129
|
+
attestationObject: b64urlEncode(concat(
|
|
130
|
+
new Uint8Array([0xa3]), // map(3)
|
|
131
|
+
new Uint8Array([0x63]), enc.encode('fmt'), // "fmt"
|
|
132
|
+
new Uint8Array([0x64]), enc.encode('none'), // "none"
|
|
133
|
+
new Uint8Array([0x67]), enc.encode('attStmt'), // "attStmt"
|
|
134
|
+
new Uint8Array([0xa0]), // {}
|
|
135
|
+
new Uint8Array([0x68]), enc.encode('authData'), // "authData"
|
|
136
|
+
new Uint8Array([0x59]), lengthBytes(authData.length), authData, // bytes(len)
|
|
137
|
+
)),
|
|
138
|
+
publicKeyAlgorithm: -7,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function lengthBytes (n) {
|
|
145
|
+
const b = new Uint8Array(2)
|
|
146
|
+
new DataView(b.buffer).setUint16(0, n, false)
|
|
147
|
+
return b
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Firma un reto con una passkey guardada: `sign(authenticatorData || sha256(clientDataJSON))`.
|
|
152
|
+
* Devuelve también el contador nuevo, que hay que guardar.
|
|
153
|
+
*/
|
|
154
|
+
export async function signAssertion ({ entry, origin, challenge }) {
|
|
155
|
+
const jwk = JSON.parse(entry.privateKey)
|
|
156
|
+
const key = await subtle().importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign'])
|
|
157
|
+
|
|
158
|
+
const signCount = (entry.signCount || 0) + 1
|
|
159
|
+
const authData = await makeAuthenticatorData({
|
|
160
|
+
rpId: entry.rpId,
|
|
161
|
+
flags: FLAGS.UP | FLAGS.UV,
|
|
162
|
+
signCount,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const clientDataJSON = enc.encode(JSON.stringify({
|
|
166
|
+
type: 'webauthn.get',
|
|
167
|
+
challenge: typeof challenge === 'string' ? challenge : b64urlEncode(challenge),
|
|
168
|
+
origin,
|
|
169
|
+
crossOrigin: false,
|
|
170
|
+
}))
|
|
171
|
+
|
|
172
|
+
const signed = concat(authData, await sha256(clientDataJSON))
|
|
173
|
+
const raw = new Uint8Array(await subtle().sign(
|
|
174
|
+
{ name: 'ECDSA', hash: 'SHA-256' }, key, signed))
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
signCount,
|
|
178
|
+
response: {
|
|
179
|
+
id: entry.credentialId,
|
|
180
|
+
rawId: entry.credentialId,
|
|
181
|
+
type: 'public-key',
|
|
182
|
+
response: {
|
|
183
|
+
authenticatorData: b64urlEncode(authData),
|
|
184
|
+
clientDataJSON: b64urlEncode(clientDataJSON),
|
|
185
|
+
// WebAuthn pide la firma en DER, no en el crudo de WebCrypto.
|
|
186
|
+
signature: b64urlEncode(rawSignatureToDer(raw)),
|
|
187
|
+
userHandle: entry.userHandle || null,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* WebCrypto firma ECDSA como `r || s` de 32 bytes cada uno; WebAuthn espera DER
|
|
195
|
+
* (SEQUENCE de dos INTEGER). Sin esta conversión el servidor rechaza la firma sin
|
|
196
|
+
* decir por qué, que es de los fallos más caros de diagnosticar.
|
|
197
|
+
*/
|
|
198
|
+
export function rawSignatureToDer (raw) {
|
|
199
|
+
const r = derInteger(raw.slice(0, 32))
|
|
200
|
+
const s = derInteger(raw.slice(32, 64))
|
|
201
|
+
return concat(new Uint8Array([0x30, r.length + s.length]), r, s)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function derInteger (bytes) {
|
|
205
|
+
let i = 0
|
|
206
|
+
while (i < bytes.length - 1 && bytes[i] === 0) i++ // sin ceros a la izquierda
|
|
207
|
+
let v = bytes.slice(i)
|
|
208
|
+
if (v[0] & 0x80) v = concat(new Uint8Array([0]), v) // positivo: byte de relleno
|
|
209
|
+
return concat(new Uint8Array([0x02, v.length]), v)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** ¿Sirve esta passkey para el sitio que la pide? */
|
|
213
|
+
export function credentialMatches (entry, rpId, allowCredentials) {
|
|
214
|
+
if (!entry?.webauthn) return false
|
|
215
|
+
if (entry.webauthn.rpId !== rpId) return false
|
|
216
|
+
if (!allowCredentials?.length) return true
|
|
217
|
+
return allowCredentials.some(c => {
|
|
218
|
+
const id = typeof c.id === 'string' ? c.id : b64urlEncode(c.id)
|
|
219
|
+
return id === entry.webauthn.credentialId
|
|
220
|
+
})
|
|
221
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/proxy-client@0.32.
|
|
1
|
+
Copia vendorizada de @dotrino/proxy-client@0.32.2 (dotrino-proxy-client/src/{canonical,client,encpub,index,sealing,signature,stats-log,stats,webrtc}.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
|
sealing.js resuelve @dotrino/identity/content de forma PEREZOSA (= ../../content.js
|
|
4
4
|
por el import map): solo se carga si de verdad se sella algo.
|
|
@@ -67,6 +67,12 @@ export class WebSocketProxyClient {
|
|
|
67
67
|
this.autoReconnect = options.autoReconnect !== false
|
|
68
68
|
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5
|
|
69
69
|
this.reconnectDelay = options.reconnectDelay ?? 3000
|
|
70
|
+
// UN SOCKET QUE NO ABRE NI FALLA. El WebSocket de Node (y el del navegador) no tiene
|
|
71
|
+
// tope para el saludo: si el TCP entra y nadie contesta el upgrade, se queda en
|
|
72
|
+
// CONNECTING sin disparar `open`, `error` ni `close`. Como el siguiente reintento solo
|
|
73
|
+
// se programa desde esos eventos, la cadena de reconexión se paraba ahí, en silencio.
|
|
74
|
+
// Se abandona el intento pasado este tiempo y cuenta como un fallo más.
|
|
75
|
+
this.connectTimeout = options.connectTimeout ?? 15000
|
|
70
76
|
this.enableWebRTC = options.enableWebRTC !== false
|
|
71
77
|
this.iceServers = options.iceServers || null
|
|
72
78
|
|
|
@@ -225,6 +231,8 @@ export class WebSocketProxyClient {
|
|
|
225
231
|
connect () {
|
|
226
232
|
return new Promise((resolve, reject) => {
|
|
227
233
|
if (this._connected) return resolve(this.token)
|
|
234
|
+
// Un reintento que estaba esperando su turno sobra: este `connect()` ya abre uno.
|
|
235
|
+
if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null }
|
|
228
236
|
this._connectResolve = resolve
|
|
229
237
|
this._connectReject = reject
|
|
230
238
|
this._open()
|
|
@@ -1242,11 +1250,21 @@ export class WebSocketProxyClient {
|
|
|
1242
1250
|
|
|
1243
1251
|
_open () {
|
|
1244
1252
|
registerTransport(this)
|
|
1253
|
+
// El intento anterior, si sigue a medio abrir, se suelta: sus eventos tardíos ya se
|
|
1254
|
+
// ignoran (`ws !== this.ws`), pero el socket quedaría abierto sin dueño.
|
|
1255
|
+
const prev = this.ws
|
|
1256
|
+
if (prev && prev.readyState === 0 /* CONNECTING */) {
|
|
1257
|
+
this.ws = null // antes de cerrarlo: su `close` ya no es de este cliente
|
|
1258
|
+
try { prev.close() } catch (_) {}
|
|
1259
|
+
}
|
|
1245
1260
|
const ws = new WebSocket(this.url)
|
|
1246
1261
|
this.ws = ws
|
|
1262
|
+
const openTimer = this.connectTimeout > 0 ? setTimeout(() => this._onConnectTimeout(ws), this.connectTimeout) : null
|
|
1263
|
+
openTimer?.unref?.()
|
|
1247
1264
|
// `ws !== this.ws` ⇒ es un socket que ya abandonamos (p.ej. por heartbeat
|
|
1248
1265
|
// muerto). Ignoramos sus eventos tardíos para no disparar reconexiones dobles.
|
|
1249
1266
|
ws.addEventListener('open', () => {
|
|
1267
|
+
if (openTimer) clearTimeout(openTimer)
|
|
1250
1268
|
if (ws !== this.ws) return
|
|
1251
1269
|
this._connected = true
|
|
1252
1270
|
this._reconnectAttempts = 0
|
|
@@ -1269,6 +1287,7 @@ export class WebSocketProxyClient {
|
|
|
1269
1287
|
}
|
|
1270
1288
|
})
|
|
1271
1289
|
ws.addEventListener('close', (ev) => {
|
|
1290
|
+
if (openTimer) clearTimeout(openTimer)
|
|
1272
1291
|
if (ws !== this.ws) return
|
|
1273
1292
|
this._stopHeartbeat()
|
|
1274
1293
|
const wasConnected = this._connected
|
|
@@ -1301,6 +1320,23 @@ export class WebSocketProxyClient {
|
|
|
1301
1320
|
})
|
|
1302
1321
|
}
|
|
1303
1322
|
|
|
1323
|
+
// El intento no abrió ni falló a tiempo: se suelta y se trata como un cierre fallido.
|
|
1324
|
+
_onConnectTimeout (ws) {
|
|
1325
|
+
if (ws !== this.ws || this._connected) return
|
|
1326
|
+
this.ws = null // a partir de acá, los eventos tardíos de `ws` se ignoran
|
|
1327
|
+
try { ws.close() } catch (_) {}
|
|
1328
|
+
const err = errorCon(`could not open the connection to ${this.url} in ${Math.round(this.connectTimeout / 1000)}s`, 'CONNECT_TIMEOUT')
|
|
1329
|
+
this._emit('error', { type: 'connect_timeout', error: err })
|
|
1330
|
+
if (this._connectReject) {
|
|
1331
|
+
this._connectReject(err)
|
|
1332
|
+
this._connectResolve = null
|
|
1333
|
+
this._connectReject = null
|
|
1334
|
+
}
|
|
1335
|
+
// Lo mismo que en `close`: se sigue reintentando solo si esto ya era un reintento. Un
|
|
1336
|
+
// `connect()` inicial que no abre se rechaza y ya.
|
|
1337
|
+
if (this._reconnectAttempts > 0 && this.autoReconnect) this._scheduleReconnect()
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1304
1340
|
// ---------- heartbeat ----------
|
|
1305
1341
|
|
|
1306
1342
|
_startHeartbeat () {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
Copia vendorizada de @dotrino/roadmap@0.4.13 (dotrino-roadmap/src/{index,manifests}.js).
|
|
2
|
+
NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
|
|
3
|
+
El registro de versiones rotas que viaja con cada versión. Importa @dotrino/compat/ranges.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dotrino/roadmap — EL REGISTRO COMÚN, COMO PIEZA DE CONSULTA.
|
|
3
|
+
*
|
|
4
|
+
* Los manifiestos de este repo dicen **qué versión va cada producto, qué protocolo habla,
|
|
5
|
+
* qué rangos necesita de los demás y qué versiones se saben rotas**. Esta librería es la
|
|
6
|
+
* forma de leerlos: cada producto la importa en vez de abrir el JSON a mano, porque
|
|
7
|
+
* treinta piezas parseando el mismo archivo a su manera son treinta formas de equivocarse
|
|
8
|
+
* y ninguna hace ruido cuando se equivoca.
|
|
9
|
+
*
|
|
10
|
+
* REPARTO DE TRABAJO, para que no se mezcle:
|
|
11
|
+
*
|
|
12
|
+
* · **`@dotrino/compat`** decide. Es la lógica: declarar, comparar, avisar.
|
|
13
|
+
* · **esto** es el registro. Son los datos, y se editan aquí.
|
|
14
|
+
*
|
|
15
|
+
* Va como `peerDependency` de `@dotrino/compat` a propósito: un pilar dentro de otro se
|
|
16
|
+
* cuela anidado a cada consumidor, y ya pasó (`PENDIENTES.md`, «pilar anidado»).
|
|
17
|
+
*
|
|
18
|
+
* EL MANIFIESTO ES POLÍTICA DEL CÓDIGO (dueño, 2026-09-04): viaja con la versión que lo
|
|
19
|
+
* trae. Lo que llega por la red es el **aviso** firmado de `@dotrino/compat/advisory`, que
|
|
20
|
+
* suma incompatibilidades y no quita ninguna.
|
|
21
|
+
*/
|
|
22
|
+
import { satisfies } from '@dotrino/compat/ranges'
|
|
23
|
+
import { MANIFESTS } from './manifests.js'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* NADA DE DISCO, y esto costó un binario que no arrancaba.
|
|
27
|
+
*
|
|
28
|
+
* La primera versión leía los JSON con `fileURLToPath(import.meta.url)` calculado AL
|
|
29
|
+
* IMPORTARSE. En un ejecutable único eso no existe: la bóveda murió al arrancar con «The
|
|
30
|
+
* "path" argument must be of type string», y en un navegador no habría arrancado nunca.
|
|
31
|
+
*
|
|
32
|
+
* Los consumen ~30 PWAs, varios daemons y un binario. Un registro que hay que leer del
|
|
33
|
+
* disco no sirve para eso, así que los manifiestos van DENTRO del código
|
|
34
|
+
* (`src/manifests.js`, generado desde `manifests/*.json`).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Los manifiestos que trae esta versión. Hay uno («dotrino») y caben varios. */
|
|
38
|
+
export function manifestNames () {
|
|
39
|
+
return Object.keys(MANIFESTS).sort()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Un manifiesto por nombre. LANZA si no está: quien pregunta por un registro que no existe
|
|
44
|
+
* tiene un error, y devolverle un objeto vacío lo convertiría en «todo compatible».
|
|
45
|
+
*/
|
|
46
|
+
export function loadManifest (name = 'dotrino') {
|
|
47
|
+
const m = Object.hasOwn(MANIFESTS, String(name)) ? MANIFESTS[String(name)] : null
|
|
48
|
+
if (!m) throw new Error(`roadmap: there is no manifest called "${name}"`)
|
|
49
|
+
return m
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Los productos que el registro conoce. */
|
|
53
|
+
export const products = (m) => Object.keys(m?.products || {}).sort()
|
|
54
|
+
|
|
55
|
+
/** La ficha de un producto, o `null` si el registro no lo conoce. */
|
|
56
|
+
export const productOf = (m, product) => m?.products?.[product] || null
|
|
57
|
+
|
|
58
|
+
/** La versión que el registro da por vigente. `null` si no lo conoce. */
|
|
59
|
+
export const currentOf = (m, product) => productOf(m, product)?.current || null
|
|
60
|
+
|
|
61
|
+
/** El rango que `product` necesita de `peer`, o `null` si no exige nada. */
|
|
62
|
+
export function requirementFor (m, product, peer) {
|
|
63
|
+
const p = productOf(m, product)
|
|
64
|
+
if (!p) return null
|
|
65
|
+
return p.requires?.[peer] ?? null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* ¿La versión de `peer` cumple lo que `product` necesita?
|
|
70
|
+
*
|
|
71
|
+
* Tres respuestas y ninguna es «bueno, adelante»:
|
|
72
|
+
* · `true` — cumple.
|
|
73
|
+
* · `false` — no cumple, y `reason` dice qué se esperaba.
|
|
74
|
+
* · cuando el registro NO exige nada, cumple: no exigir es una decisión, no una laguna.
|
|
75
|
+
*
|
|
76
|
+
* Si el registro no conoce al producto que pregunta, es `false`: preguntar por quien no
|
|
77
|
+
* está en el registro no puede salir que sí.
|
|
78
|
+
*/
|
|
79
|
+
export function meets (m, { product, peer, version }) {
|
|
80
|
+
if (!productOf(m, product)) {
|
|
81
|
+
return { ok: false, reason: `the registry does not know "${product}"` }
|
|
82
|
+
}
|
|
83
|
+
const rango = requirementFor(m, product, peer)
|
|
84
|
+
if (rango == null) return { ok: true, reason: '' }
|
|
85
|
+
if (satisfies(version, rango)) return { ok: true, reason: '' }
|
|
86
|
+
return { ok: false, reason: `${product} needs ${peer} ${rango} and this one is ${version}` }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** La lista de rotas del registro, lista para pasarla a `check` de `@dotrino/compat`. */
|
|
90
|
+
export const brokenOf = (m) => Array.isArray(m?.broken) ? m.broken : []
|
|
91
|
+
|
|
92
|
+
/** Lo que el administrador enseña: qué versión debería ir cada pieza, según el registro. */
|
|
93
|
+
export function summary (m) {
|
|
94
|
+
return {
|
|
95
|
+
manifest: m?.manifest || null,
|
|
96
|
+
seq: m?.seq ?? null,
|
|
97
|
+
issued: m?.issued || null,
|
|
98
|
+
products: products(m).map((p) => ({
|
|
99
|
+
product: p,
|
|
100
|
+
current: currentOf(m, p),
|
|
101
|
+
protocol: productOf(m, p)?.protocol ?? null,
|
|
102
|
+
requires: productOf(m, p)?.requires || {}
|
|
103
|
+
})),
|
|
104
|
+
broken: brokenOf(m)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default { manifestNames, loadManifest, products, productOf, currentOf, requirementFor, meets, brokenOf, summary }
|