@dotrino/identity 0.121.2 → 0.121.4

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.121.2",
3
+ "version": "0.121.4",
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",
package/src/index.js CHANGED
@@ -387,7 +387,7 @@ export class Identity {
387
387
  * Une ESTE perfil a la cuenta de otro (la de una bóveda). No es adoptar una versión nueva
388
388
  * de la tuya: esta llave pasa a ser de OTRA cuenta y deja de tener la suya, así que solo
389
389
  * procede sobre un perfil creado con `{ forVault: true }`. Si no, devuelve
390
- * `{ joined: false, reason: 'perfil-con-datos' }` y no escribe nada.
390
+ * `{ joined: false, reason: 'profile-has-data' }` y no escribe nada.
391
391
  */
392
392
  async joinProfile (acta) { return this._call('joinProfile', { acta }) }
393
393
  /** Marca este perfil como nacido para ADOPTAR la cuenta de otro (camino A). */
package/vault/acta.js CHANGED
@@ -289,38 +289,38 @@ export const sealerLinkOf = (acta) => (acta?.sealerChanged && acta?.sealerLink)
289
289
  */
290
290
  export function checkSealerLink (acta) {
291
291
  const l = acta?.sealerLink
292
- if (!l) return acta?.sealerChanged && acta?.v >= 5 ? 'eslabon-ausente' : null
293
- if (l.v !== SEALER_LINK_V) return 'eslabon-version'
294
- if (typeof l.sig !== 'string') return 'eslabon-sin-firma'
295
- if (l.profileId !== acta.profileId) return 'eslabon-otro-perfil'
292
+ if (!l) return acta?.sealerChanged && acta?.v >= 5 ? 'link-missing' : null
293
+ if (l.v !== SEALER_LINK_V) return 'link-version'
294
+ if (typeof l.sig !== 'string') return 'link-unsigned'
295
+ if (l.profileId !== acta.profileId) return 'link-other-profile'
296
296
  // EL ACTA VIGENTE ARRASTRA EL ÚLTIMO ESLABÓN, que casi nunca es de ella: los selladores
297
297
  // cambian poquísimo y las actas cambian con cada emparejamiento. Así que solo cuando
298
298
  // ESTA acta es el eslabón se le exige que coincida en `seq` y en quién lo firmó; si lo
299
299
  // hereda, basta con que venga de antes.
300
300
  if (acta.sealerChanged) {
301
- if (l.seq !== acta.seq) return 'eslabon-otro-seq'
302
- if (l.by !== acta.sealedBy) return 'eslabon-otro-sellador'
301
+ if (l.seq !== acta.seq) return 'link-other-seq'
302
+ if (l.by !== acta.sealedBy) return 'link-other-sealer'
303
303
  } else if (!(l.seq < acta.seq)) {
304
- return 'eslabon-del-futuro'
304
+ return 'link-from-future'
305
305
  }
306
306
  // Y en los dos casos tiene que DECIR LO MISMO que el acta sobre quién sella: si no
307
307
  // cambiaron, el heredado los sigue describiendo. Esto es lo que impide que el acta y su
308
308
  // eslabón cuenten cosas distintas — que es todo el punto de meterlo dentro.
309
309
  const dice = [...(l.sealers || [])].slice().sort().join('|')
310
310
  const segunElActa = sealersOf(acta).slice().sort().join('|')
311
- if (dice !== segunElActa) return 'eslabon-no-cuadra'
311
+ if (dice !== segunElActa) return 'link-mismatch'
312
312
  return null
313
313
  }
314
314
 
315
315
  /** Firma del eslabón por quien sella. Se hace ANTES de firmar el acta que lo lleva. */
316
316
  export async function verifySealerLink (link) {
317
- if (!link || link.v !== SEALER_LINK_V) return { ok: false, reason: 'eslabon-version' }
318
- if (!isPub(link.profileId) || !isPub(link.by)) return { ok: false, reason: 'eslabon-forma' }
319
- if (!Array.isArray(link.sealers) || !link.sealers.length || !link.sealers.every(isPub)) return { ok: false, reason: 'eslabon-selladores' }
320
- if (typeof link.seq !== 'number' || link.seq < 1) return { ok: false, reason: 'eslabon-seq' }
321
- if (typeof link.sig !== 'string') return { ok: false, reason: 'eslabon-sin-firma' }
317
+ if (!link || link.v !== SEALER_LINK_V) return { ok: false, reason: 'link-version' }
318
+ if (!isPub(link.profileId) || !isPub(link.by)) return { ok: false, reason: 'link-shape' }
319
+ if (!Array.isArray(link.sealers) || !link.sealers.length || !link.sealers.every(isPub)) return { ok: false, reason: 'link-sealers' }
320
+ if (typeof link.seq !== 'number' || link.seq < 1) return { ok: false, reason: 'link-seq' }
321
+ if (typeof link.sig !== 'string') return { ok: false, reason: 'link-unsigned' }
322
322
  const ok = await verifyDeviceSig({ publickey: link.by, data: sealerLinkBody(link), signature: link.sig })
323
- return ok ? { ok: true } : { ok: false, reason: 'eslabon-firma-invalida' }
323
+ return ok ? { ok: true } : { ok: false, reason: 'link-invalid-signature' }
324
324
  }
325
325
 
326
326
  /**
@@ -338,29 +338,29 @@ export async function verifySealerLink (link) {
338
338
  * aceptando a un sellador retirado; para eso está mirar el registro, no más firmas.
339
339
  */
340
340
  export async function verifySealerLinkChain (chain, { expectedProfileId = null } = {}) {
341
- if (!Array.isArray(chain) || !chain.length) return { ok: false, reason: 'cadena-vacia' }
341
+ if (!Array.isArray(chain) || !chain.length) return { ok: false, reason: 'empty-chain' }
342
342
  const [raiz] = chain
343
343
  // La misma regla que en `verifySealerChain`: la raíz la ancla la llave del perfil, no el
344
344
  // número 1 (una cuenta puede haber perdido su génesis; ver allí).
345
- if (!Number.isInteger(raiz?.seq) || raiz.seq < 1) return { ok: false, reason: 'no-empieza-en-genesis' }
346
- if (raiz.by !== raiz.profileId) return { ok: false, reason: 'genesis-no-autofirmado' }
347
- if (expectedProfileId != null && raiz.profileId !== expectedProfileId) return { ok: false, reason: 'otro-perfil' }
345
+ if (!Number.isInteger(raiz?.seq) || raiz.seq < 1) return { ok: false, reason: 'no-genesis' }
346
+ if (raiz.by !== raiz.profileId) return { ok: false, reason: 'genesis-not-self-signed' }
347
+ if (expectedProfileId != null && raiz.profileId !== expectedProfileId) return { ok: false, reason: 'other-profile' }
348
348
  const vr = await verifySealerLink(raiz)
349
349
  if (!vr.ok) return { ok: false, reason: 'genesis:' + vr.reason }
350
350
 
351
351
  for (let i = 1; i < chain.length; i++) {
352
352
  const previo = chain[i - 1]
353
353
  const actual = chain[i]
354
- if (actual?.profileId !== raiz.profileId) return { ok: false, reason: `eslabon-${i}:otro-perfil` }
355
- if (!(actual.seq > previo.seq)) return { ok: false, reason: `eslabon-${i}:seq-no-crece` }
354
+ if (actual?.profileId !== raiz.profileId) return { ok: false, reason: `link-${i}:other-profile` }
355
+ if (!(actual.seq > previo.seq)) return { ok: false, reason: `link-${i}:seq-not-increasing` }
356
356
  const v = await verifySealerLink(actual)
357
- if (!v.ok) return { ok: false, reason: `eslabon-${i}:` + v.reason }
357
+ if (!v.ok) return { ok: false, reason: `link-${i}:` + v.reason }
358
358
  const a = actual.prev
359
359
  if (!a || a.seq !== previo.seq || a.hash !== await sealerLinkHash(previo)) {
360
- return { ok: false, reason: `eslabon-${i}:no-encadena` }
360
+ return { ok: false, reason: `link-${i}:not-chained` }
361
361
  }
362
362
  // Y lo que da la autoridad: quien lo firmó podía sellar SEGÚN EL ESLABÓN ANTERIOR.
363
- if (!previo.sealers.includes(actual.by)) return { ok: false, reason: `eslabon-${i}:sellador-no-autorizado` }
363
+ if (!previo.sealers.includes(actual.by)) return { ok: false, reason: `link-${i}:unauthorized-sealer` }
364
364
  }
365
365
  const ultimo = chain[chain.length - 1]
366
366
  return { ok: true, profileId: raiz.profileId, seq: ultimo.seq, sealers: [...ultimo.sealers] }
@@ -473,7 +473,7 @@ export function checkShape (acta) {
473
473
  if (acta.chainUrl != null && !isChainUrl(acta.chainUrl)) return 'chainurl'
474
474
  // El campo solo existe —y solo se exige— en las viejas.
475
475
  if (conCampoSellador(acta) && !isPub(acta.sealer)) return 'shape'
476
- if (!conCampoSellador(acta) && acta.sealer != null) return 'sealer-no-va-en-v3'
476
+ if (!conCampoSellador(acta) && acta.sealer != null) return 'sealer-not-in-v3'
477
477
  if (!Number.isInteger(acta.seq) || acta.seq < 1) return 'seq'
478
478
  if (acta.seq > 1 && typeof acta.prev !== 'string') return 'prev'
479
479
  if (!Array.isArray(acta.members) || acta.members.length === 0) return 'members'
@@ -507,7 +507,7 @@ export function checkShape (acta) {
507
507
  // El CN es la frontera: un SERVICIO solo puede abrir su propio cajón, y un
508
508
  // DISPOSITIVO no tiene cajón que abrir. Que no se pueda escribir un acta que
509
509
  // mezcle las dos cosas es lo que hace que el límite sea real y no una costumbre.
510
- if (m.cn != null && !isValidCn(m.cn)) return 'cn-invalido'
510
+ if (m.cn != null && !isValidCn(m.cn)) return 'invalid-cn'
511
511
  // La llave de cifrado: por ahora se valida la FORMA y solo si viene. NO se exige
512
512
  // todavía, aunque el destino sea exigírsela a quien recibe secretos.
513
513
  //
@@ -516,27 +516,27 @@ export function checkShape (acta) {
516
516
  // entró sin ella —que son todas las anteriores a esto— y el vault dejaría de
517
517
  // arrancar en vez de avisar. Primero los servicios registran su llave (op
518
518
  // `encpub`), y cuando no quede ninguno sin ella se aprieta aquí.
519
- if (m.encPub != null && !isEncPub(m.encPub)) return 'encpub-invalido'
519
+ if (m.encPub != null && !isEncPub(m.encPub)) return 'invalid-encpub'
520
520
  }
521
521
  // LA LLAVE DE SELLADO Y SU REGISTRO. `sealPub` puede no estar (un perfil que no sella
522
522
  // secretos), pero si está tiene que decir DESDE QUÉ acta manda: sin `sealSince` no se
523
523
  // puede decidir con qué llave verificar un sobre viejo.
524
524
  if (acta.v >= 2) {
525
525
  if (acta.sealPub != null) {
526
- if (!isPub(acta.sealPub)) return 'sealpub-invalido'
526
+ if (!isPub(acta.sealPub)) return 'invalid-sealpub'
527
527
  if (!Number.isInteger(acta.sealSince) || acta.sealSince < 1 || acta.sealSince > acta.seq) return 'sealsince'
528
528
  } else if (acta.sealSince) return 'sealsince'
529
529
  if (!Array.isArray(acta.sealKeys)) return 'sealkeys'
530
530
  for (const k of acta.sealKeys) {
531
- if (!isPub(k?.pub)) return 'sealkey-invalida'
532
- if (!Number.isInteger(k.from) || !Number.isInteger(k.to) || k.from < 1 || k.to < k.from) return 'sealkey-rango'
531
+ if (!isPub(k?.pub)) return 'invalid-sealkey'
532
+ if (!Number.isInteger(k.from) || !Number.isInteger(k.to) || k.from < 1 || k.to < k.from) return 'sealkey-range'
533
533
  }
534
534
  }
535
- if (new Set(acta.members.map((m) => m.pub)).size !== acta.members.length) return 'miembro-duplicado'
535
+ if (new Set(acta.members.map((m) => m.pub)).size !== acta.members.length) return 'duplicate-member'
536
536
  // SIN NADIE QUE SELLE, el acta está muerta: no habría forma de volver a cambiarla nunca.
537
537
  // Es el hermano del cierre de `sign`, y sustituye a «el sellador tiene que ser miembro»
538
538
  // ahora que sellar es solo un permiso y los permisos ya son de miembros por definición.
539
- if (!sealersOf(acta).length) return 'sin-sellador'
539
+ if (!sealersOf(acta).length) return 'no-sealer'
540
540
  // La cadena de selladores: el génesis es el ancla (`null`); cualquier otra apunta a un
541
541
  // eslabón anterior. Sin `hash` no se puede encadenar nada, así que se exige la forma.
542
542
  if (acta.sealerAnchor != null) {
@@ -549,7 +549,7 @@ export function checkShape (acta) {
549
549
  // inválida. Lo que sostiene la cadena no es el ancla sino que **cada eslabón lo selle
550
550
  // alguien a quien el anterior autorizaba**; el ancla añade que no se pueda empalmar un
551
551
  // eslabón de otra rama, y se comprueba cuando viene (ver `verifySealerChain`).
552
- if (!acta.members.some((m) => m.caps.includes('sign'))) return 'sin-firmante'
552
+ if (!acta.members.some((m) => m.caps.includes('sign'))) return 'no-signer'
553
553
  return null
554
554
  }
555
555
 
@@ -627,20 +627,20 @@ export async function verifySignedBy (args = {}) {
627
627
  return { ok: false, reason: 'shape' }
628
628
  }
629
629
  const c = await verifySealerChain(chain, { expectedProfileId })
630
- if (!c.ok) return { ok: false, reason: 'cadena:' + c.reason }
630
+ if (!c.ok) return { ok: false, reason: 'chain:' + c.reason }
631
631
  const cabeza = chain[chain.length - 1]
632
632
  // Quien firmó tiene que ser miembro CON `sign` en el acta vigente de esa cadena. Si le
633
633
  // quitaste el permiso, sus firmas dejan de contar en cuanto se ve un acta más nueva.
634
634
  // Un SERVICIO (miembro con `cn`) solo firma dentro de su cajón: quien verifica dice cuál
635
635
  // con `ns` (el directorio de nombres acepta lo firmado por su aparato `names`, no por
636
636
  // cualquier servicio de la cuenta). Sin `ns`, un servicio nunca pasa, como siempre.
637
- if (!memberCanSign(cabeza, publickey, ns)) return { ok: false, reason: 'firmante-no-autorizado' }
638
- if (!(await verifyDeviceSig({ publickey, data, signature }))) return { ok: false, reason: 'firma-invalida' }
637
+ if (!memberCanSign(cabeza, publickey, ns)) return { ok: false, reason: 'unauthorized-signer' }
638
+ if (!(await verifyDeviceSig({ publickey, data, signature }))) return { ok: false, reason: 'invalid-signature' }
639
639
  return { ok: true, profileId: c.profileId, seq: c.seq, signer: publickey }
640
640
  }
641
641
 
642
642
  export async function verifySealerChain (chain, { expectedProfileId = null } = {}) {
643
- if (!Array.isArray(chain) || !chain.length) return { ok: false, reason: 'cadena-vacia' }
643
+ if (!Array.isArray(chain) || !chain.length) return { ok: false, reason: 'empty-chain' }
644
644
  const [raiz] = chain
645
645
 
646
646
  // EL ANCLA ES LA LLAVE DEL PERFIL, NO EL NÚMERO 1 (dueño, 2026-10-14). La raíz vale si
@@ -651,18 +651,18 @@ export async function verifySealerChain (chain, { expectedProfileId = null } = {
651
651
  // perfil fabrica un génesis alterno igual que un acta posterior, y contra eso la capa es
652
652
  // el registro público. Lo que sí sostiene la cadena —que cada eslabón lo firmó alguien a
653
653
  // quien el anterior autorizaba— sigue intacto.
654
- if (!Number.isInteger(raiz.seq) || raiz.seq < 1) return { ok: false, reason: 'no-empieza-en-genesis' }
655
- if (raiz.sealedBy !== raiz.profileId) return { ok: false, reason: 'genesis-no-autofirmado' }
656
- if (expectedProfileId != null && raiz.profileId !== expectedProfileId) return { ok: false, reason: 'otro-perfil' }
654
+ if (!Number.isInteger(raiz.seq) || raiz.seq < 1) return { ok: false, reason: 'no-genesis' }
655
+ if (raiz.sealedBy !== raiz.profileId) return { ok: false, reason: 'genesis-not-self-signed' }
656
+ if (expectedProfileId != null && raiz.profileId !== expectedProfileId) return { ok: false, reason: 'other-profile' }
657
657
  const vr = await verifyActa({ acta: raiz })
658
658
  if (!vr.ok) return { ok: false, reason: 'genesis:' + vr.reason }
659
659
 
660
660
  for (let i = 1; i < chain.length; i++) {
661
661
  const previo = chain[i - 1]
662
662
  const actual = chain[i]
663
- if (actual.profileId !== raiz.profileId) return { ok: false, reason: 'otro-perfil' }
663
+ if (actual.profileId !== raiz.profileId) return { ok: false, reason: 'other-profile' }
664
664
  const v = await verifyActa({ acta: actual })
665
- if (!v.ok) return { ok: false, reason: `eslabon-${i}:` + v.reason }
665
+ if (!v.ok) return { ok: false, reason: `link-${i}:` + v.reason }
666
666
  // Encadena con el anterior: mismo `seq` y mismo hash. Sin el hash bastaría con
667
667
  // acertar un número para colar un eslabón de otra rama.
668
668
  // El ancla, SI viene: fija que este eslabón sale del anterior y no de otra rama. Una
@@ -670,10 +670,10 @@ export async function verifySealerChain (chain, { expectedProfileId = null } = {
670
670
  // abajo — que es lo que de verdad impide falsificarla.
671
671
  const a = actual.sealerAnchor
672
672
  if (a && (a.seq !== previo.seq || a.hash !== await actaHash(previo))) {
673
- return { ok: false, reason: `eslabon-${i}:no-encadena` }
673
+ return { ok: false, reason: `link-${i}:not-chained` }
674
674
  }
675
675
  // Y lo que da la autoridad: quien la selló tenía permiso SEGÚN EL ESLABÓN ANTERIOR.
676
- if (!canSeal(previo, actual.sealedBy)) return { ok: false, reason: `eslabon-${i}:sellador-no-autorizado` }
676
+ if (!canSeal(previo, actual.sealedBy)) return { ok: false, reason: `link-${i}:unauthorized-sealer` }
677
677
  }
678
678
  const ultima = chain[chain.length - 1]
679
679
  return { ok: true, profileId: raiz.profileId, seq: ultima.seq, sealers: sealersOf(ultima) }
@@ -682,10 +682,10 @@ export async function verifySealerChain (chain, { expectedProfileId = null } = {
682
682
  export async function verifyActa ({ acta, expectedProfileId } = {}) {
683
683
  const shape = checkShape(acta)
684
684
  if (shape) return { ok: false, reason: shape }
685
- if (typeof acta.sig !== 'string') return { ok: false, reason: 'sin-firma' }
686
- if (expectedProfileId != null && acta.profileId !== expectedProfileId) return { ok: false, reason: 'otro-perfil' }
685
+ if (typeof acta.sig !== 'string') return { ok: false, reason: 'unsigned' }
686
+ if (expectedProfileId != null && acta.profileId !== expectedProfileId) return { ok: false, reason: 'other-profile' }
687
687
  const ok = await verifyDeviceSig({ publickey: acta.sealedBy, data: actaBody(acta), signature: acta.sig })
688
- if (!ok) return { ok: false, reason: 'firma-invalida' }
688
+ if (!ok) return { ok: false, reason: 'invalid-signature' }
689
689
  // El eslabón va DENTRO y la firma de arriba lo cubre, pero eso solo prueba que está ahí.
690
690
  // Que diga lo mismo que el acta es lo que hace verdad «ya está validado dentro».
691
691
  const mal = checkSealerLink(acta)
@@ -720,21 +720,21 @@ export async function verifyActa ({ acta, expectedProfileId } = {}) {
720
720
  * @returns {Promise<{ok:true}|{ok:false, reason:string}>}
721
721
  */
722
722
  export async function checkVaultReply ({ acta, cert, vault, sub, scope = null, justSealed = false } = {}) {
723
- if (!acta) return { ok: false, reason: 'sin-acta' }
724
- if (!isPub(vault)) return { ok: false, reason: 'sin-boveda' }
723
+ if (!acta) return { ok: false, reason: 'no-record' }
724
+ if (!isPub(vault)) return { ok: false, reason: 'no-vault-key' }
725
725
  // Para QUÉ llave es el papel no es opcional: sin ella `verifyDelegation` no lo mira, y un
726
726
  // papel emitido para otra llave pasaría por bueno.
727
- if (!isPub(sub)) return { ok: false, reason: 'sin-llave-del-aparato' }
727
+ if (!isPub(sub)) return { ok: false, reason: 'no-device-key' }
728
728
  const v = await verifyActa({ acta })
729
- if (!v.ok) return { ok: false, reason: 'acta:' + v.reason }
730
- if (!canSeal(acta, vault)) return { ok: false, reason: 'boveda-no-sella' }
731
- if (justSealed && !samePubkey(acta.sealedBy, vault)) return { ok: false, reason: 'acta-de-otra-selladora' }
732
- if (!cert || !samePubkey(cert.iss, vault)) return { ok: false, reason: 'papel-de-otra-llave' }
729
+ if (!v.ok) return { ok: false, reason: 'record:' + v.reason }
730
+ if (!canSeal(acta, vault)) return { ok: false, reason: 'vault-not-sealer' }
731
+ if (justSealed && !samePubkey(acta.sealedBy, vault)) return { ok: false, reason: 'record-from-other-sealer' }
732
+ if (!cert || !samePubkey(cert.iss, vault)) return { ok: false, reason: 'cert-from-other-key' }
733
733
  const d = await verifyDelegation({
734
734
  cert, expectedSub: sub, actaSeq: acta.seq, sealers: sealersOf(acta),
735
735
  ...(scope ? { expectedScope: scope } : {})
736
736
  })
737
- if (!d.ok) return { ok: false, reason: 'papel:' + d.reason }
737
+ if (!d.ok) return { ok: false, reason: 'cert:' + d.reason }
738
738
  return { ok: true }
739
739
  }
740
740
 
@@ -1213,16 +1213,16 @@ export async function verifyProfileCard (card) {
1213
1213
  * anterior — así nadie te cuela dispositivos ajenos en el perfil de tu contacto,
1214
1214
  * y nunca se retrocede a una versión vieja.
1215
1215
  * Si el master cambió legítimamente (traspaso), la tarjeta nueva la firma el entrante y
1216
- * hay que re-confirmarla: se devuelve `master-cambiado` para que la app lo diga en vez de
1216
+ * hay que re-confirmarla: se devuelve `master-changed` para que la app lo diga en vez de
1217
1217
  * aceptarlo en silencio.
1218
1218
  */
1219
1219
  export async function canAdoptCard ({ card, current }) {
1220
- if (!(await verifyProfileCard(card))) return { adopt: false, reason: 'firma-invalida' }
1221
- if (current && current.profileId !== card.profileId) return { adopt: false, reason: 'otro-perfil' }
1222
- if (!current) return { adopt: true, reason: 'primera-vez' }
1223
- if (card.seq < current.seq) return { adopt: false, reason: 'seq-menor' }
1224
- if (card.sealedBy !== current.sealedBy) return { adopt: false, reason: 'master-cambiado' }
1225
- return { adopt: true, reason: card.seq > current.seq ? 'seq-mayor' : 'igual' }
1220
+ if (!(await verifyProfileCard(card))) return { adopt: false, reason: 'invalid-signature' }
1221
+ if (current && current.profileId !== card.profileId) return { adopt: false, reason: 'other-profile' }
1222
+ if (!current) return { adopt: true, reason: 'first-time' }
1223
+ if (card.seq < current.seq) return { adopt: false, reason: 'lower-seq' }
1224
+ if (card.sealedBy !== current.sealedBy) return { adopt: false, reason: 'master-changed' }
1225
+ return { adopt: true, reason: card.seq > current.seq ? 'higher-seq' : 'same' }
1226
1226
  }
1227
1227
 
1228
1228
  // ----- continuidad: unir una identidad que ya existía -----
@@ -1270,19 +1270,19 @@ export async function verifyContinuity (record) {
1270
1270
  export async function canAdopt ({ candidate, current }) {
1271
1271
  const v = await verifyActa({ acta: candidate, expectedProfileId: current?.profileId })
1272
1272
  if (!v.ok) return { adopt: false, reason: v.reason }
1273
- if (!current) return { adopt: true, reason: 'sin-acta-previa' }
1273
+ if (!current) return { adopt: true, reason: 'no-previous-record' }
1274
1274
 
1275
1275
  if (candidate.seq > current.seq) {
1276
- if (!canSeal(current, candidate.sealedBy)) return { adopt: false, reason: 'sellador-no-autorizado' }
1276
+ if (!canSeal(current, candidate.sealedBy)) return { adopt: false, reason: 'unauthorized-sealer' }
1277
1277
  if (candidate.seq === current.seq + 1 && candidate.prev !== await actaHash(current)) {
1278
- return { adopt: false, reason: 'no-encadena' }
1278
+ return { adopt: false, reason: 'not-chained' }
1279
1279
  }
1280
- return { adopt: true, reason: 'seq-mayor' }
1280
+ return { adopt: true, reason: 'higher-seq' }
1281
1281
  }
1282
1282
 
1283
1283
  if (candidate.seq === current.seq) {
1284
1284
  const [hCan, hCur] = [await actaHash(candidate), await actaHash(current)]
1285
- if (hCan === hCur) return { adopt: false, reason: 'misma-acta' }
1285
+ if (hCan === hCur) return { adopt: false, reason: 'same-record' }
1286
1286
  // A igual `seq`, con multivault, las dos ramas pueden venir de selladores DISTINTOS y
1287
1287
  // ser las dos legítimas. Antes era imposible y se rechazaba de plano; ahora se
1288
1288
  // desempata con las reglas de abajo, que ya estaban escritas.
@@ -1294,16 +1294,16 @@ export async function canAdopt ({ candidate, current }) {
1294
1294
  // correcta es por el acta PADRE, que aquí no se tiene; tener el mismo `sealedBy` es
1295
1295
  // exactamente lo que dice que las dos salieron de ella.
1296
1296
  const mismoFirmante = candidate.sealedBy === current.sealedBy
1297
- if (!mismoFirmante && !canSeal(current, candidate.sealedBy)) return { adopt: false, reason: 'otro-sellador' }
1297
+ if (!mismoFirmante && !canSeal(current, candidate.sealedBy)) return { adopt: false, reason: 'other-sealer' }
1298
1298
  // SOLO QUEDA EL HASH. Antes había una regla más —«gana la que traspasa»— que existía
1299
1299
  // para arbitrar el campo `sealer`: dos ramas que cedían el mando a destinos distintos.
1300
1300
  // Ese campo ya no existe (dueño, 2026-08-31: «SEAL es el nuevo master»), y con él se
1301
1301
  // va la regla y la ambigüedad que arrastraba: `sealer !== sealedBy` significaba
1302
1302
  // «traspaso» con un solo master y dejó de significarlo con varios selladores.
1303
- return hCan < hCur ? { adopt: true, reason: 'desempate-hash' } : { adopt: false, reason: 'desempate-hash' }
1303
+ return hCan < hCur ? { adopt: true, reason: 'hash-tiebreak' } : { adopt: false, reason: 'hash-tiebreak' }
1304
1304
  }
1305
1305
 
1306
- return { adopt: false, reason: 'seq-menor' }
1306
+ return { adopt: false, reason: 'lower-seq' }
1307
1307
  }
1308
1308
 
1309
1309
  export default {
@@ -145,31 +145,31 @@ export async function verifyAssertion (assertion, opts = {}) {
145
145
  if (!Array.isArray(a.scopes) || (a.claims != null && typeof a.claims !== 'object')) return { ok: false, reason: 'shape' }
146
146
  if (typeof a.signature !== 'string' || typeof a.publickey !== 'string') return { ok: false, reason: 'shape' }
147
147
 
148
- if (a.aud !== audience.trim()) return { ok: false, reason: 'otro-destinatario' }
149
- if (a.nonce !== nonce) return { ok: false, reason: 'otro-reto' }
148
+ if (a.aud !== audience.trim()) return { ok: false, reason: 'other-audience' }
149
+ if (a.nonce !== nonce) return { ok: false, reason: 'other-nonce' }
150
150
 
151
- if (a.exp <= a.iat) return { ok: false, reason: 'vigencia-invalida' }
151
+ if (a.exp <= a.iat) return { ok: false, reason: 'invalid-lifetime' }
152
152
  // El TOPE lo comprueba quien recibe. Si no, el emisor decide solo cuánto dura su
153
153
  // credencial y el tope no es un tope.
154
- if (a.exp - a.iat > ASSERTION_MAX_TTL_MS) return { ok: false, reason: 'vigencia-excesiva' }
154
+ if (a.exp - a.iat > ASSERTION_MAX_TTL_MS) return { ok: false, reason: 'lifetime-too-long' }
155
155
  // Y el vencimiento se juzga SIN margen: el margen es para el arranque (relojes que
156
156
  // difieren), no para seguir aceptando lo que ya venció.
157
- if (a.exp <= now) return { ok: false, reason: 'vencida' }
158
- if (a.iat > now + maxSkewMs) return { ok: false, reason: 'del-futuro' }
157
+ if (a.exp <= now) return { ok: false, reason: 'expired' }
158
+ if (a.iat > now + maxSkewMs) return { ok: false, reason: 'from-future' }
159
159
 
160
- if (a.scopes.some((s) => !SCOPES.includes(s))) return { ok: false, reason: 'alcance-desconocido' }
160
+ if (a.scopes.some((s) => !SCOPES.includes(s))) return { ok: false, reason: 'unknown-scope' }
161
161
  const permitido = claimsAllowed(a.scopes)
162
- if (Object.keys(a.claims || {}).some((k) => !permitido.has(k))) return { ok: false, reason: 'claim-sin-alcance' }
162
+ if (Object.keys(a.claims || {}).some((k) => !permitido.has(k))) return { ok: false, reason: 'claim-without-scope' }
163
163
 
164
164
  // Y lo de siempre: que la firma sea de alguien a quien el acta de esa cadena autoriza a
165
165
  // firmar por esta identidad. Es la misma comprobación que para cualquier contenido
166
166
  // firmado; aquí no se inventa una cripto aparte.
167
167
  const v = await verifySignedBy({ data: bodyOf(a), signature: a.signature, publickey: a.publickey, chain: a.chain, expectedProfileId })
168
- if (!v.ok) return { ok: false, reason: 'firma:' + v.reason }
168
+ if (!v.ok) return { ok: false, reason: 'signature:' + v.reason }
169
169
  // A NOMBRE DE QUIÉN dice ir, contra a nombre de quién va de verdad. Sin esto una prueba
170
170
  // podría afirmar ser de otro perfil y la firma seguiría cuadrando: diría la verdad sobre
171
171
  // quién la firmó y una mentira sobre de quién es.
172
- if (a.sub !== v.profileId) return { ok: false, reason: 'otro-sujeto' }
172
+ if (a.sub !== v.profileId) return { ok: false, reason: 'other-subject' }
173
173
 
174
174
  return { ok: true, profileId: v.profileId, signer: v.signer, seq: v.seq, scopes: [...a.scopes], claims: { ...(a.claims || {}) }, aud: a.aud, exp: a.exp }
175
175
  }
@@ -200,18 +200,18 @@ export async function verifySignedFor (args = {}) {
200
200
  const { data, signature, publickey, chain, audience, expectedProfileId = null, ns = null, now = Date.now(), maxSkewMs = ASSERTION_MAX_SKEW_MS } = args
201
201
  if (typeof audience !== 'string' || !audience.trim()) return { ok: false, reason: 'no-audience' }
202
202
  if (!data || typeof data !== 'object') return { ok: false, reason: 'shape' }
203
- if (typeof data.aud !== 'string' || !data.aud) return { ok: false, reason: 'sin-destinatario' }
204
- if (data.aud !== audience.trim()) return { ok: false, reason: 'otro-destinatario' }
203
+ if (typeof data.aud !== 'string' || !data.aud) return { ok: false, reason: 'no-aud' }
204
+ if (data.aud !== audience.trim()) return { ok: false, reason: 'other-audience' }
205
205
  if (data.exp != null) {
206
206
  if (!Number.isFinite(data.exp)) return { ok: false, reason: 'shape' }
207
- if (data.exp <= now) return { ok: false, reason: 'vencida' }
207
+ if (data.exp <= now) return { ok: false, reason: 'expired' }
208
208
  }
209
209
  if (data.iat != null) {
210
210
  if (!Number.isFinite(data.iat)) return { ok: false, reason: 'shape' }
211
- if (data.iat > now + maxSkewMs) return { ok: false, reason: 'del-futuro' }
211
+ if (data.iat > now + maxSkewMs) return { ok: false, reason: 'from-future' }
212
212
  }
213
213
  const v = await verifySignedBy({ data, signature, publickey, chain, expectedProfileId, ns })
214
- if (!v.ok) return { ok: false, reason: 'firma:' + v.reason }
214
+ if (!v.ok) return { ok: false, reason: 'signature:' + v.reason }
215
215
  return { ok: true, profileId: v.profileId, signer: v.signer, seq: v.seq, aud: data.aud }
216
216
  }
217
217
 
@@ -30,7 +30,7 @@ const SIGN = { name: 'ECDSA', hash: { name: 'SHA-256' } }
30
30
  /*
31
31
  * EL MODELO VIEJO YA NO ENTRA. Los papeles de antes de 0.73 llevaban `exp` y no `seq`; se
32
32
  * aceptaron hasta el 2026-10-01 por un repliegue de migración declarado y con fecha, y esa
33
- * fecha pasó: se quitó (2026-10-05). Un papel sin `seq` se rechaza con `legacy-cert-retirado`,
33
+ * fecha pasó: se quitó (2026-10-05). Un papel sin `seq` se rechaza con `legacy-cert-retired`,
34
34
  * el mismo código que daba el repliegue al caducar, así que quien lo comprueba no cambia.
35
35
  */
36
36
 
@@ -238,7 +238,7 @@ export async function verifyDelegation ({ cert, expectedScope, expectedSub, acta
238
238
  if (v !== 1 || typeof iss !== 'string' || typeof sub !== 'string' || typeof sig !== 'string') return { ok: false, reason: 'shape' }
239
239
  if (typeof iat !== 'number' || (typeof scope !== 'string' && !Array.isArray(scope))) return { ok: false, reason: 'shape' }
240
240
  // UN PAPEL DEL MODELO VIEJO (con `exp`, sin `seq`) ya no vale: el repliegue se retiró.
241
- if (typeof seq !== 'number') return { ok: false, reason: typeof cert.exp === 'number' ? 'legacy-cert-retirado' : 'shape' }
241
+ if (typeof seq !== 'number') return { ok: false, reason: typeof cert.exp === 'number' ? 'legacy-cert-retired' : 'shape' }
242
242
  if (!(await rawVerify(iss, enc(canonicalStringify(delegationBody(cert))), sig))) return { ok: false, reason: 'bad-signature' }
243
243
  // EL ACTA MANDA, Y NO HAY RELOJ (dueño, 2026-08-31). El papel no vence: dice «una
244
244
  // selladora de este perfil, mirando el acta nº `seq`, avaló esta llave». Lo que puede
@@ -264,7 +264,7 @@ export async function verifyDelegation ({ cert, expectedScope, expectedSub, acta
264
264
  // estaba apagado: vuelve, su papel ya no vale, y renovarlo exige una selladora ABIERTA.
265
265
  // O sea que un cambio de acta te obligaría a abrir la bóveda para que tus aparatos
266
266
  // volvieran — justo lo que se acaba de quitar de en medio.
267
- if (seq > actaSeq) return { ok: false, reason: 'acta-vieja' }
267
+ if (seq > actaSeq) return { ok: false, reason: 'stale-record' }
268
268
  // Y QUIEN LO EMITIÓ TIENE QUE PODER SELLAR. No se compara contra «la maestra»: cualquiera
269
269
  // que el acta nombre sellador emite papeles válidos — si no, la segunda bóveda podría
270
270
  // invalidar todos los certificados al sellar y luego no poder dar los nuevos.
package/vault/core.js CHANGED
@@ -408,7 +408,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
408
408
  */
409
409
  async function sealMasterKey () {
410
410
  const raw = kv.getItem(KEY_STORAGE)
411
- if (!raw || !keyLock?.seal) return { ok: false, reason: 'sin-candado' }
411
+ if (!raw || !keyLock?.seal) return { ok: false, reason: 'no-lock' }
412
412
  const guardado = JSON.parse(raw)
413
413
  if (guardado.sealed) return { ok: true, already: true }
414
414
  await guardarPar(KEY_STORAGE, guardado.privateJwk, guardado.publicJwk)
@@ -1253,7 +1253,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
1253
1253
  * La plaza que quede abierta allí se cierra desde la consola, y así está dicho en el diseño.
1254
1254
  */
1255
1255
  async function closeLoginOn (meta) {
1256
- if (!meta?.user || !meta?.sid) return { ok: false, reason: 'sin-inicio-de-sesion' }
1256
+ if (!meta?.user || !meta?.sid) return { ok: false, reason: 'no-login' }
1257
1257
  let client = null
1258
1258
  try {
1259
1259
  const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
@@ -1292,9 +1292,9 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
1292
1292
  */
1293
1293
  async function joinProfile (candidate) {
1294
1294
  const v = await Acta.verifyActa({ acta: candidate })
1295
- if (!v.ok) return { joined: false, reason: 'acta-invalida:' + v.reason }
1295
+ if (!v.ok) return { joined: false, reason: 'invalid-record:' + v.reason }
1296
1296
  if (!candidate.members.some((m) => m.pub === publickeyJwkStr)) {
1297
- return { joined: false, reason: 'no-soy-miembro' }
1297
+ return { joined: false, reason: 'not-a-member' }
1298
1298
  }
1299
1299
  const current = loadActa()
1300
1300
 
@@ -1307,7 +1307,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
1307
1307
  if (current && !isPendingJoin()) {
1308
1308
  return {
1309
1309
  joined: false,
1310
- reason: 'perfil-con-datos',
1310
+ reason: 'profile-has-data',
1311
1311
  profileId: current.profileId,
1312
1312
  seq: current.seq,
1313
1313
  members: current.members.length
@@ -1347,7 +1347,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
1347
1347
  kv.setItem(VAULT_DEVICE_STORAGE, JSON.stringify({ useIdentityKey: true, publickey: publickeyJwkStr }))
1348
1348
  kv.setItem(VAULT_CERT_STORAGE, JSON.stringify({ cert: res.cert, master: res.master, proxy: res.proxy, deviceId: res.deviceId, pairedAt: Date.now() }))
1349
1349
  // Conectarse a una bóveda es ENTRAR A SU CUENTA: el acta viene con el cert.
1350
- const unido = res.acta ? await joinProfile(res.acta) : { joined: false, reason: 'sin-acta' }
1350
+ const unido = res.acta ? await joinProfile(res.acta) : { joined: false, reason: 'no-record' }
1351
1351
  emitVault({ phase: 'paired', deviceId: res.deviceId, master: res.master, join: unido })
1352
1352
  // LA LLAVE ES DEL CHIP DEL TELÉFONO (app de Android): la app guarda la cuenta para su
1353
1353
  // pantalla nativa de Pedidos, con el MISMO papel. Así el teléfono es un solo aparato en
@@ -1380,7 +1380,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
1380
1380
  const r = await adoptActa(a)
1381
1381
  if (r.adopted) last = r
1382
1382
  }
1383
- return last || { adopted: false, reason: 'nada-que-adoptar', seq: loadActa()?.seq ?? null }
1383
+ return last || { adopted: false, reason: 'nothing-to-adopt', seq: loadActa()?.seq ?? null }
1384
1384
  }
1385
1385
 
1386
1386
  /**
@@ -1416,7 +1416,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
1416
1416
  // Ya no hay ventana de caducidad: el papel no vence. Lo único que obliga a pedir uno
1417
1417
  // nuevo es que el ACTA diga algo distinto de lo que lleva escrito.
1418
1418
  /** Rechazos de la bóveda que se arreglan con un papel nuevo (el mismo juego que la app nativa). */
1419
- const PAPER_REASONS = new Set(['scope', 'acta-vieja', 'untrusted-issuer', 'expired', 'legacy-cert-retirado', 'no-acta'])
1419
+ const PAPER_REASONS = new Set(['scope', 'stale-record', 'untrusted-issuer', 'expired', 'legacy-cert-retired', 'no-acta'])
1420
1420
  const RENEW_RETRY_MS = 60 * 60 * 1000 // si falla (vault apagado), no insistir >1 vez/hora
1421
1421
  let renewLastTry = 0
1422
1422
  /**
@@ -2328,7 +2328,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
2328
2328
  // selladora de ESTE perfil, mirando ESTA acta, avaló esta llave», y sin acta no se
2329
2329
  // puede decir ninguna de las dos cosas.
2330
2330
  const acta = loadActa()
2331
- if (!acta) throw Object.assign(new Error('this profile has no record to issue against'), { code: 'sin-acta' })
2331
+ if (!acta) throw Object.assign(new Error('this profile has no record to issue against'), { code: 'no-record' })
2332
2332
  // `iss` se FUERZA a la propia maestra: el usuario no puede emitir cert para otro emisor.
2333
2333
  const cert = await signDelegationWith(masterKey(), publickeyJwkStr, { sub, scope, iat, seq: acta.seq, nonce: nonce || crypto.randomUUID() })
2334
2334
  const store = loadDelegations()
@@ -3024,11 +3024,11 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
3024
3024
  // La bóveda devuelve el acta ya sellada por ella (y con los certs re-emitidos): se
3025
3025
  // adopta por las reglas de siempre (§2.4.1) — encaja porque el sellador es el que
3026
3026
  // este mismo aparato nombró hace un momento.
3027
- // `misma-acta` = la bóveda guardó exactamente la que este aparato acababa de sellar
3027
+ // `same-record` = la bóveda guardó exactamente la que este aparato acababa de sellar
3028
3028
  // y no la cambió. No hay nada que adoptar, y es justo lo que se esperaba: el
3029
3029
  // traspaso ya iba dentro de esa acta.
3030
3030
  const r = await adoptActa(res.acta)
3031
- const ok = r.adopted || r.reason === 'misma-acta'
3031
+ const ok = r.adopted || r.reason === 'same-record'
3032
3032
  emitVault({ phase: 'adopted', master: res.master, seq: res.acta?.seq, ok })
3033
3033
  if (!ok) throw new Error('the vault returned a record that does not fit: ' + r.reason)
3034
3034
  return { ok: true, adopted: true, profileId: mine.profileId, seq: r.seq ?? res.acta?.seq, master: res.master, deviceId: res.deviceId }
package/vault/session.js CHANGED
@@ -117,39 +117,39 @@ export async function verifySession (paper, { chain, expectedProfileId = null, o
117
117
  if (!isStr(p.sid) || !isStr(p.s) || !isStr(p.by) || !isStr(p.origin) || !isStr(p.sig)) return { ok: false, reason: 'shape' }
118
118
  if (!Number.isFinite(p.iat) || !Number.isFinite(p.exp) || !Array.isArray(p.scopes)) return { ok: false, reason: 'shape' }
119
119
 
120
- if (p.exp <= p.iat) return { ok: false, reason: 'vigencia-invalida' }
121
- if (p.exp - p.iat > SESSION_MAX_TTL_MS) return { ok: false, reason: 'vigencia-excesiva' }
122
- if (p.exp <= now) return { ok: false, reason: 'vencida' }
123
- if (p.iat > now + maxSkewMs) return { ok: false, reason: 'del-futuro' }
120
+ if (p.exp <= p.iat) return { ok: false, reason: 'invalid-lifetime' }
121
+ if (p.exp - p.iat > SESSION_MAX_TTL_MS) return { ok: false, reason: 'lifetime-too-long' }
122
+ if (p.exp <= now) return { ok: false, reason: 'expired' }
123
+ if (p.iat > now + maxSkewMs) return { ok: false, reason: 'from-future' }
124
124
 
125
125
  // DÓNDE vale. Un papel para una aplicación no vale en otra: el origen va firmado dentro.
126
- if (origin != null && p.origin !== String(origin).trim()) return { ok: false, reason: 'otro-origen' }
126
+ if (origin != null && p.origin !== String(origin).trim()) return { ok: false, reason: 'other-origin' }
127
127
 
128
- if (p.scopes.some((s) => !SESSION_SCOPES.includes(s))) return { ok: false, reason: 'alcance-desconocido' }
129
- if (p.scopes.some((s) => SESSION_FORBIDDEN.includes(s))) return { ok: false, reason: 'alcance-prohibido' }
128
+ if (p.scopes.some((s) => !SESSION_SCOPES.includes(s))) return { ok: false, reason: 'unknown-scope' }
129
+ if (p.scopes.some((s) => SESSION_FORBIDDEN.includes(s))) return { ok: false, reason: 'forbidden-scope' }
130
130
 
131
131
  // EL ACTA MANDA, y por eso hace falta: dice si el aparato que respalda sigue siendo del
132
132
  // perfil y qué puede. Sin ella no se juzga, en vez de dar por bueno lo que diga el papel.
133
133
  const c = await verifySealerChain(chain, { expectedProfileId })
134
- if (!c.ok) return { ok: false, reason: 'cadena:' + c.reason }
134
+ if (!c.ok) return { ok: false, reason: 'chain:' + c.reason }
135
135
  const acta = chain[chain.length - 1]
136
136
 
137
137
  // PRIMERO, ¿ES DE LA CASA? Y en este orden a propósito: si al aparato lo quitaron, el
138
138
  // motivo tiene que decir eso y no «le falta un permiso», que manda a mirar al sitio
139
139
  // equivocado. Es además el invariante que hace barata la revocación — quitar un aparato
140
140
  // se lleva sus sesiones sin avisar a nadie ni perseguir papeles.
141
- if (!(acta.members || []).some((m) => m?.pub === p.by)) return { ok: false, reason: 'aparato-no-es-del-perfil' }
141
+ if (!(acta.members || []).some((m) => m?.pub === p.by)) return { ok: false, reason: 'device-not-in-profile' }
142
142
 
143
143
  // Y DESPUÉS, NUNCA AMPLÍA: cada alcance exige que el aparato que firmó lo tenga HOY. Se
144
144
  // comprueba aquí y no solo al emitir, porque quien emite es precisamente quien podría
145
145
  // mentir — y porque el acta de hoy puede haberle quitado lo que tenía ayer.
146
146
  for (const s of p.scopes) {
147
147
  const cap = SCOPE_NEEDS[s]
148
- if (cap && !memberCan(acta, p.by, cap)) return { ok: false, reason: 'aparato-sin-' + cap }
148
+ if (cap && !memberCan(acta, p.by, cap)) return { ok: false, reason: 'device-lacks-' + cap }
149
149
  }
150
150
 
151
151
  const { sig, ...body } = p
152
- if (!(await verifyDeviceSig({ publickey: p.by, data: body, signature: sig }))) return { ok: false, reason: 'firma-invalida' }
152
+ if (!(await verifyDeviceSig({ publickey: p.by, data: body, signature: sig }))) return { ok: false, reason: 'invalid-signature' }
153
153
 
154
154
  return { ok: true, profileId: c.profileId, seq: c.seq, sid: p.sid, s: p.s, by: p.by, origin: p.origin, scopes: [...p.scopes], exp: p.exp }
155
155
  }
@@ -164,8 +164,8 @@ export async function verifySessionSigned ({ data, signature, session, chain, sc
164
164
  if (!data || !isStr(signature)) return { ok: false, reason: 'shape' }
165
165
  const v = await verifySession(session, { chain, expectedProfileId, origin, now })
166
166
  if (!v.ok) return v
167
- if (scope && !v.scopes.includes(scope)) return { ok: false, reason: 'fuera-de-alcance' }
168
- if (!(await verifyDeviceSig({ publickey: v.s, data, signature }))) return { ok: false, reason: 'firma-invalida' }
167
+ if (scope && !v.scopes.includes(scope)) return { ok: false, reason: 'out-of-scope' }
168
+ if (!(await verifyDeviceSig({ publickey: v.s, data, signature }))) return { ok: false, reason: 'invalid-signature' }
169
169
  return { ok: true, profileId: v.profileId, sid: v.sid, scopes: v.scopes, exp: v.exp }
170
170
  }
171
171
 
@@ -81,18 +81,18 @@ export async function openSession ({ transport, sessionPubkey, encPub, origin, s
81
81
  return await new Promise((resolve, reject) => {
82
82
  let listo = false
83
83
  const fin = (fn, arg) => { if (!listo) { listo = true; clearTimeout(reloj); off?.(); fn(arg) } }
84
- const reloj = setTimeout(() => fin(reject, Object.assign(new Error('nadie abrió la sesión a tiempo'), { code: 'session-timeout' })), timeoutMs)
84
+ const reloj = setTimeout(() => fin(reject, Object.assign(new Error('openSession: nobody granted the session in time'), { code: 'session-timeout' })), timeoutMs)
85
85
  const off = transport.on('message', async (_from, payload, meta) => {
86
86
  const p = typeof payload === 'string' ? (() => { try { return JSON.parse(payload) } catch (_) { return null } })() : payload
87
87
  if (!p || p.sid !== sid) return
88
- if (p.op === SESSION_OP.DENY) return fin(reject, Object.assign(new Error('la sesión no se concedió'), { code: 'session-denied' }))
88
+ if (p.op === SESSION_OP.DENY) return fin(reject, Object.assign(new Error('openSession: the session was denied'), { code: 'session-denied' }))
89
89
  if (p.op !== SESSION_OP.GRANT) return
90
90
  // NO SE ACEPTA LO QUE VENGA EN CLARO. El sellado es del pilar del transporte; aquí
91
91
  // solo se comprueba que llegó sellado, que es lo que la app puede saber.
92
92
  if (meta && meta.sealed === false) return
93
93
  const v = await verifySession(p.paper, { chain: p.chain, origin: origin.trim() })
94
- if (!v.ok) return fin(reject, Object.assign(new Error('el papel de sesión no vale: ' + v.reason), { code: 'session-invalid' }))
95
- if (p.paper.s !== sessionPubkey) return fin(reject, Object.assign(new Error('el papel es para otra llave'), { code: 'session-invalid' }))
94
+ if (!v.ok) return fin(reject, Object.assign(new Error('openSession: the session paper does not verify: ' + v.reason), { code: 'session-invalid' }))
95
+ if (p.paper.s !== sessionPubkey) return fin(reject, Object.assign(new Error('openSession: the session paper is for another key'), { code: 'session-invalid' }))
96
96
  fin(resolve, { paper: p.paper, chain: p.chain, profileId: v.profileId, sid, scopes: v.scopes, exp: v.exp })
97
97
  })
98
98
  })
@@ -109,7 +109,7 @@ export async function openSession ({ transport, sessionPubkey, encPub, origin, s
109
109
  */
110
110
  export async function grantSession ({ transport, invite, by, sign, chain, scopes, ttlMs = SESSION_DEFAULT_TTL_MS, now = Date.now() } = {}) {
111
111
  const inv = parseInvite(invite)
112
- if (!inv) throw new Error('grantSession: invitación ilegible')
112
+ if (!inv) throw Object.assign(new Error('grantSession: unreadable invite'), { code: 'bad-invite' })
113
113
  if (typeof by !== 'string' || !by) throw new Error('grantSession: by (this device pubkey) required')
114
114
  if (typeof sign !== 'function') throw new Error('grantSession: sign(body) required')
115
115
  if (!Array.isArray(chain) || !chain.length) throw new Error('grantSession: chain required')
@@ -123,7 +123,7 @@ export async function grantSession ({ transport, invite, by, sign, chain, scopes
123
123
  /** Decir que no, en vez de dejar al otro mirando una pantalla que no avanza. */
124
124
  export async function denySession ({ transport, invite } = {}) {
125
125
  const inv = parseInvite(invite)
126
- if (!inv) throw new Error('denySession: invitación ilegible')
126
+ if (!inv) throw Object.assign(new Error('denySession: unreadable invite'), { code: 'bad-invite' })
127
127
  await enviar(transport, inv, { op: SESSION_OP.DENY, sid: inv.sid })
128
128
  }
129
129
 
@@ -144,7 +144,7 @@ async function enviar (transport, inv, payload) {
144
144
  }
145
145
  // Sin llave de cifrado del otro lado no se puede sellar. Se dice en vez de mandarlo en
146
146
  // claro por su cuenta: quien llama decide si eso le vale.
147
- if (!inv.encPub) throw Object.assign(new Error('sessionFlow: la invitación no trae encPub; no se puede sellar'), { code: 'unsealed' })
147
+ if (!inv.encPub) throw Object.assign(new Error('sessionFlow: the invite carries no encPub; cannot seal'), { code: 'unsealed' })
148
148
  return transport.sendByPubkey(inv.s, payload)
149
149
  }
150
150
 
@@ -1,3 +1,3 @@
1
- Copia vendorizada de @dotrino/compat@0.4.0 (dotrino-compat/src/{advisory,index,ranges}.js).
1
+ Copia vendorizada de @dotrino/compat@0.4.1 (dotrino-compat/src/{advisory,index,ranges}.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
  Qué versión es cada pieza y si pueden hablar (CONVENCIONES §14).
@@ -105,14 +105,14 @@ export async function verifyAdvisory ({ advisory, publickey }) {
105
105
  * Devuelve el aviso vigente (el nuevo o el que ya tenía) y por qué.
106
106
  */
107
107
  export async function adoptAdvisory ({ current, incoming, publickey }) {
108
- if (!isAdvisory(incoming)) return { advisory: current || null, adopted: false, reason: 'no-es-un-aviso' }
108
+ if (!isAdvisory(incoming)) return { advisory: current || null, adopted: false, reason: 'not-an-advisory' }
109
109
  if (current && incoming.seq <= current.seq) {
110
- return { advisory: current, adopted: false, reason: 'no-es-mas-nuevo' }
110
+ return { advisory: current, adopted: false, reason: 'not-newer' }
111
111
  }
112
112
  if (!(await verifyAdvisory({ advisory: incoming, publickey }))) {
113
- return { advisory: current || null, adopted: false, reason: 'firma-invalida' }
113
+ return { advisory: current || null, adopted: false, reason: 'invalid-signature' }
114
114
  }
115
- return { advisory: incoming, adopted: true, reason: current ? 'mas-nuevo' : 'primero' }
115
+ return { advisory: incoming, adopted: true, reason: current ? 'newer' : 'first' }
116
116
  }
117
117
 
118
118
  /**
@@ -1,3 +1,3 @@
1
- Copia vendorizada de @dotrino/roadmap@0.4.32 (dotrino-roadmap/src/{index,manifests}.js).
1
+ Copia vendorizada de @dotrino/roadmap@0.4.35 (dotrino-roadmap/src/{index,manifests}.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
  El registro de versiones rotas que viaja con cada versión. Importa @dotrino/compat/ranges.
@@ -8,8 +8,8 @@
8
8
  export const MANIFESTS = Object.freeze({
9
9
  "dotrino": {
10
10
  "manifest": "dotrino",
11
- "seq": 31,
12
- "issued": "2026-10-16",
11
+ "seq": 32,
12
+ "issued": "2026-10-18",
13
13
  "note": "Registro comun de compatibilidad del ecosistema. Se edita a mano en este repo y se consulta con @dotrino/roadmap. Rangos: ver @dotrino/compat/ranges (x.y.z, >= > <= <, «a - b», «*», o una lista que es un O).",
14
14
  "products": {
15
15
  "compat": {
@@ -22,16 +22,16 @@ export const MANIFESTS = Object.freeze({
22
22
  "env": {
23
23
  "repo": "imdotrino/dotrino-vault",
24
24
  "npm": "@dotrino/env",
25
- "current": "0.91.1",
25
+ "current": "0.92.0",
26
26
  "protocol": 1,
27
27
  "requires": {
28
- "vault": ">=0.91.1"
28
+ "vault": ">=0.92.0"
29
29
  }
30
30
  },
31
31
  "identity": {
32
32
  "repo": "imdotrino/dotrino-identity",
33
33
  "npm": "@dotrino/identity",
34
- "current": "0.120.0",
34
+ "current": "0.121.3",
35
35
  "protocol": 1,
36
36
  "requires": {
37
37
  "proxy-client": ">=0.13.1"
@@ -101,7 +101,7 @@ export const MANIFESTS = Object.freeze({
101
101
  "profile": {
102
102
  "repo": "imdotrino/dotrino-profile",
103
103
  "npm": "@dotrino/profile",
104
- "current": "0.20.2",
104
+ "current": "0.20.3",
105
105
  "protocol": 1,
106
106
  "requires": {
107
107
  "identity": ">=0.33.0"
@@ -131,7 +131,7 @@ export const MANIFESTS = Object.freeze({
131
131
  "remote-agent": {
132
132
  "repo": "imdotrino/dotrino-remote-agent",
133
133
  "npm": "@dotrino/remote-agent",
134
- "current": "0.22.1",
134
+ "current": "0.22.2",
135
135
  "protocol": 1,
136
136
  "requires": {
137
137
  "identity": ">=0.120.0",
@@ -193,10 +193,10 @@ export const MANIFESTS = Object.freeze({
193
193
  "content": {
194
194
  "repo": "imdotrino/dotrino-content",
195
195
  "npm": "@dotrino/content",
196
- "current": "0.6.6",
196
+ "current": "0.6.7",
197
197
  "protocol": 1,
198
198
  "requires": {
199
- "remote-agent": ">=0.22.0",
199
+ "remote-agent": ">=0.22.2",
200
200
  "identity": ">=0.120.0",
201
201
  "vault": ">=0.87.0",
202
202
  "proxy-client": ">=0.36.1"
@@ -253,7 +253,7 @@ export const MANIFESTS = Object.freeze({
253
253
  "vault": {
254
254
  "repo": "imdotrino/dotrino-vault",
255
255
  "npm": "@dotrino/vault",
256
- "current": "0.91.1",
256
+ "current": "0.92.0",
257
257
  "protocol": 1,
258
258
  "requires": {
259
259
  "vaultd": ">=0.105.0",
@@ -269,7 +269,7 @@ export const MANIFESTS = Object.freeze({
269
269
  "vaultd": {
270
270
  "repo": "imdotrino/dotrino-vault",
271
271
  "npm": "@dotrino/vaultd",
272
- "current": "0.163.4",
272
+ "current": "0.164.1",
273
273
  "protocol": 1,
274
274
  "requires": {
275
275
  "identity": ">=0.118.0",
@@ -360,6 +360,17 @@ export const MANIFESTS = Object.freeze({
360
360
  "proxy-client": ">=0.36.1",
361
361
  "remote-agent": ">=0.20.1"
362
362
  }
363
+ },
364
+ "accounts": {
365
+ "repo": "imdotrino/dotrino-accounts",
366
+ "npm": null,
367
+ "current": "0.1.0",
368
+ "protocol": 1,
369
+ "requires": {
370
+ "identity": ">=0.116.0",
371
+ "remote-agent": ">=0.19.0",
372
+ "vault": ">=0.92.0"
373
+ }
363
374
  }
364
375
  },
365
376
  "broken": [