@dotrino/identity 0.63.0 → 0.64.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/identity",
3
- "version": "0.63.0",
3
+ "version": "0.64.0",
4
4
  "description": "Identidad y rating de usuarios compartidos entre apps de Dotrino (vault iframe + postMessage)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -196,6 +196,25 @@ export async function signWithDevice ({ privateJwk, privateKey, publickey, data
196
196
  * 5) `nonce` no revocado (`revoked`: fn(nonce)→bool, Set o mapa).
197
197
  * @returns {{ok:boolean, reason?:string, iss?, sub?, scope?, iat?, exp?, nonce?}}
198
198
  */
199
+ /**
200
+ * MARGEN DE RELOJ ENTRE DOS APARATOS. Sin esto, emparejar es una lotería.
201
+ *
202
+ * La bóveda sella el certificado con SU reloj (`iat`) y el aparato lo valida con el suyo.
203
+ * Con tolerancia cero, un aparato que vaya un pelo por detrás rechaza un certificado
204
+ * perfectamente bueno con `not-yet-valid` — y el usuario no ve un problema de hora, ve un
205
+ * emparejamiento que no funciona. Pasó de verdad (2026-08-31): un teléfono **850 ms** por
206
+ * detrás, medido, no podía enrolarse en ninguna bóveda.
207
+ *
208
+ * Dos minutos es generoso para un reloj a la deriva y no significa nada frente a un
209
+ * certificado que vale 30 días: aceptarlo dos minutos antes de tiempo no le abre la puerta
210
+ * a nadie, porque lo que autoriza es la FIRMA, no la hora.
211
+ *
212
+ * Es para comparar aparatos DISTINTOS. Donde el mismo operador controla los dos relojes
213
+ * —un servicio validando en la máquina que emite— el estricto sigue siendo lo correcto, y
214
+ * por eso el default de `verifyDelegation` no cambia.
215
+ */
216
+ export const PEER_SKEW_MS = 120_000
217
+
199
218
  export async function verifyDelegation ({ cert, expectedScope, expectedSub, now = Date.now(), skewMs = 0, revoked } = {}) {
200
219
  if (!cert || typeof cert !== 'object') return { ok: false, reason: 'no-cert' }
201
220
  const { v, iss, sub, scope, iat, exp, nonce, sig } = cert
package/vault/remote.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * No reimplementa cripto: usa `@dotrino/identity/capabilities`. Transporte:
14
14
  * `@dotrino/proxy-client` (importado perezosamente; solo se carga al emparejar).
15
15
  */
16
- import { makeDeviceKey, signWithDevice, verifyDelegation, verifyDeviceSig, makePairingCode, commitCode, pubkeyId } from './capabilities.js'
16
+ import { makeDeviceKey, signWithDevice, verifyDelegation, verifyDeviceSig, makePairingCode, commitCode, pubkeyId, PEER_SKEW_MS } from './capabilities.js'
17
17
 
18
18
  const MSG = {
19
19
  HELLO: 'vault.hello',
@@ -208,7 +208,9 @@ export async function enrollDevice ({ qr, device, onChallenge, label = '', conti
208
208
  }
209
209
 
210
210
  // Validación estricta antes de guardar (cierra inyección de cert / sustitución de maestra).
211
- const v = await verifyDelegation({ cert: res.cert, expectedSub: dev.publickey })
211
+ // `PEER_SKEW_MS`: el cert lo acaba de sellar la bóveda con SU reloj y lo valida ESTE
212
+ // aparato con el suyo. Sin margen, ir 850 ms por detrás bastaba para no poder enrolarse.
213
+ const v = await verifyDelegation({ cert: res.cert, expectedSub: dev.publickey, skewMs: PEER_SKEW_MS })
212
214
  if (!v.ok) throw new Error('invalid cert: ' + v.reason)
213
215
  if (res.cert.iss !== qr.iss) throw new Error('cert signed by a master key different from the one you saw')
214
216
  if (res.cert.sub !== dev.publickey) throw new Error('cert issued for a different device')