@dotrino/identity 0.73.1 → 0.74.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.73.1",
3
+ "version": "0.74.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",
@@ -27,6 +27,28 @@ const ECDSA = { name: 'ECDSA', namedCurve: 'P-256' }
27
27
  const ECDH = { name: 'ECDH', namedCurve: 'P-256' }
28
28
  const SIGN = { name: 'ECDSA', hash: { name: 'SHA-256' } }
29
29
 
30
+ /**
31
+ * HASTA CUÁNDO SE ACEPTA UN PAPEL DEL MODELO VIEJO — **2026-10-01**, y se quita ese día.
32
+ *
33
+ * Los certificados de antes de 0.73 llevaban `exp` y no `seq`. Al pasar la bóveda al modelo
34
+ * del acta, TODOS los aparatos ya enrolados se quedaban fuera de golpe (`shape`) y sin forma
35
+ * de volver: el mostrador de renovación los rechazaba por lo mismo. Diez máquinas —los dos
36
+ * proxios, los nodes de contenido, el bot— habrían necesitado re-enrolarse a mano, con TTY
37
+ * en los dos lados.
38
+ *
39
+ * Así que se aceptan, con dos frenos y ninguna concesión de fondo:
40
+ * · hasta su PROPIO vencimiento, que sigue valiendo (el más lejano es 2026-09-30);
41
+ * · y en ningún caso pasado este tope.
42
+ * Además el mostrador los cruza con el acta igual que a los nuevos, así que un papel viejo
43
+ * no autoriza nada que el acta de hoy no autorice.
44
+ *
45
+ * Se apaga solo por partida doble: ninguna bóveda 0.73+ puede EMITIR uno de estos
46
+ * (`signDelegationWith` solo escribe `seq`), así que cuando el último caduque esta rama es
47
+ * inalcanzable. **Y aun así se borra en la fecha**, porque un repliegue sin fecha no es una
48
+ * migración: es un agujero (`CLAUDE.md`).
49
+ */
50
+ export const LEGACY_CERTS_UNTIL = Date.parse('2026-10-01T00:00:00Z')
51
+
30
52
  const enc = (s) => new TextEncoder().encode(s)
31
53
 
32
54
  async function rawSign (privateKey, bytes) {
@@ -113,6 +135,13 @@ export { avatarSvg, avatarDataUri } from './avatar.js'
113
135
  * ninguna renovación), y nadie tiene que abrir nada por calendario.
114
136
  */
115
137
  export function delegationBody (cert) {
138
+ // LA FORMA DEL CUERPO ES LA DEL PAPEL, no la del código que lo lee. Un papel del modelo
139
+ // viejo se firmó sobre `{ …, exp, nonce }`; recomponerlo con `seq: undefined` da otra
140
+ // string canónica y la firma no cuadra NUNCA — que es como el repliegue de migración no
141
+ // servía de nada hasta que una prueba lo dijo.
142
+ if (typeof cert.seq !== 'number' && typeof cert.exp === 'number') {
143
+ return { v: cert.v, iss: cert.iss, sub: cert.sub, scope: cert.scope, iat: cert.iat, exp: cert.exp, nonce: cert.nonce }
144
+ }
116
145
  return { v: cert.v, iss: cert.iss, sub: cert.sub, scope: cert.scope, iat: cert.iat, seq: cert.seq, nonce: cert.nonce }
117
146
  }
118
147
 
@@ -208,7 +237,10 @@ export async function verifyDelegation ({ cert, expectedScope, expectedSub, acta
208
237
  if (!cert || typeof cert !== 'object') return { ok: false, reason: 'no-cert' }
209
238
  const { v, iss, sub, scope, iat, seq, nonce, sig } = cert
210
239
  if (v !== 1 || typeof iss !== 'string' || typeof sub !== 'string' || typeof sig !== 'string') return { ok: false, reason: 'shape' }
211
- if (typeof iat !== 'number' || typeof seq !== 'number' || (typeof scope !== 'string' && !Array.isArray(scope))) return { ok: false, reason: 'shape' }
240
+ if (typeof iat !== 'number' || (typeof scope !== 'string' && !Array.isArray(scope))) return { ok: false, reason: 'shape' }
241
+ // ¿ES UN PAPEL DEL MODELO VIEJO? Llevaba `exp` y no `seq` (ver `LEGACY_CERTS_UNTIL`).
242
+ const legado = typeof seq !== 'number'
243
+ if (legado && typeof cert.exp !== 'number') return { ok: false, reason: 'shape' }
212
244
  if (!(await rawVerify(iss, enc(canonicalStringify(delegationBody(cert))), sig))) return { ok: false, reason: 'bad-signature' }
213
245
  // EL ACTA MANDA, Y NO HAY RELOJ (dueño, 2026-08-31). El papel no vence: dice «una
214
246
  // selladora de este perfil, mirando el acta nº `seq`, avaló esta llave». Lo que puede
@@ -234,7 +266,19 @@ export async function verifyDelegation ({ cert, expectedScope, expectedSub, acta
234
266
  // estaba apagado: vuelve, su papel ya no vale, y renovarlo exige una selladora ABIERTA.
235
267
  // O sea que un cambio de acta te obligaría a abrir la bóveda para que tus aparatos
236
268
  // volvieran — justo lo que se acaba de quitar de en medio.
237
- if (seq > actaSeq) return { ok: false, reason: 'acta-vieja' }
269
+ if (!legado && seq > actaSeq) return { ok: false, reason: 'acta-vieja' }
270
+ // REPLIEGUE DE MIGRACIÓN, declarado y con fecha (ver `LEGACY_CERTS_UNTIL`). Un papel del
271
+ // modelo viejo se sigue aceptando, pero SOLO hasta su propio vencimiento: no se le regala
272
+ // ni un día. No hay nada que decidir sobre el acta que nombra, porque no nombra ninguna.
273
+ //
274
+ // Lo que NO se relaja, y es lo que hace que esto no sea un agujero: quien lo emitió tiene
275
+ // que poder sellar (abajo), y el mostrador cruza igual con el acta (`memberCanScope`). Un
276
+ // papel viejo no autoriza nada que el acta de hoy no autorice; lo único que compra es
277
+ // seguir hablando mientras su máquina se actualiza.
278
+ if (legado) {
279
+ if (Date.now() > cert.exp) return { ok: false, reason: 'expired' }
280
+ if (Date.now() > LEGACY_CERTS_UNTIL) return { ok: false, reason: 'legacy-cert-retirado' }
281
+ }
238
282
  // Y QUIEN LO EMITIÓ TIENE QUE PODER SELLAR. No se compara contra «la maestra»: cualquiera
239
283
  // que el acta nombre sellador emite papeles válidos — si no, la segunda bóveda podría
240
284
  // invalidar todos los certificados al sellar y luego no poder dar los nuevos.
@@ -246,7 +290,7 @@ export async function verifyDelegation ({ cert, expectedScope, expectedSub, acta
246
290
  : (revoked instanceof Set ? revoked.has(nonce) : !!revoked[nonce])
247
291
  if (isRev) return { ok: false, reason: 'revoked' }
248
292
  }
249
- return { ok: true, iss, sub, scope, iat, seq, nonce }
293
+ return { ok: true, iss, sub, scope, iat, seq: legado ? null : seq, nonce, legacy: legado || undefined }
250
294
  }
251
295
 
252
296
  /**