@dotrino/identity 0.112.0 → 0.114.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/README.md +5 -0
- package/package.json +1 -1
- package/vault/acta.js +20 -7
- package/vault/assertion.js +6 -3
- package/vault/core.js +8 -2
package/README.md
CHANGED
|
@@ -179,6 +179,11 @@ const v = await verifySignedFor({ data: pin, signature, publickey, chain, audien
|
|
|
179
179
|
`aud` es obligatorio igual que en la prueba; `exp` es opcional, porque la caducidad de lo
|
|
180
180
|
publicado la lleva el servicio (el TTL del pin) y no el cuerpo.
|
|
181
181
|
|
|
182
|
+
Si quien firma es un **servicio** de la cuenta (un miembro con `cn`), pásale `ns` con su
|
|
183
|
+
cajón (0.114.0+): un servicio solo firma dentro del suyo, y sin `ns` no pasa nunca. Así el
|
|
184
|
+
directorio de nombres acepta lo que firma su aparato `names` sin aceptar lo de cualquier
|
|
185
|
+
otro servicio de la misma cuenta.
|
|
186
|
+
|
|
182
187
|
### Permiso por origen (0.86.0+)
|
|
183
188
|
|
|
184
189
|
Una prueba dice **quién eres** sin preguntar nada: quien llega al iframe ya pasó el filtro
|
package/package.json
CHANGED
package/vault/acta.js
CHANGED
|
@@ -328,8 +328,9 @@ export async function verifySealerLink (link) {
|
|
|
328
328
|
*
|
|
329
329
|
* Es la hermana de `verifySealerChain`, que hace lo mismo con actas enteras para quien las
|
|
330
330
|
* tiene. Lo que comprueba, y por qué basta:
|
|
331
|
-
* 1. el primero
|
|
332
|
-
* que
|
|
331
|
+
* 1. el primero está firmado por `profileId` — el ancla, que no se puede fabricar sin la
|
|
332
|
+
* llave que da nombre al perfil. Normalmente es el génesis (`seq 1`, sin `prev`), pero
|
|
333
|
+
* no tiene por qué: una cuenta que perdió su génesis ancla en un eslabón posterior;
|
|
333
334
|
* 2. cada siguiente lo firmó alguien a quien el ANTERIOR autorizaba;
|
|
334
335
|
* 3. y apunta al anterior por `seq` + hash, así que no se cuela uno de otra rama.
|
|
335
336
|
*
|
|
@@ -339,7 +340,9 @@ export async function verifySealerLink (link) {
|
|
|
339
340
|
export async function verifySealerLinkChain (chain, { expectedProfileId = null } = {}) {
|
|
340
341
|
if (!Array.isArray(chain) || !chain.length) return { ok: false, reason: 'cadena-vacia' }
|
|
341
342
|
const [raiz] = chain
|
|
342
|
-
|
|
343
|
+
// La misma regla que en `verifySealerChain`: la raíz la ancla la llave del perfil, no el
|
|
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' }
|
|
343
346
|
if (raiz.by !== raiz.profileId) return { ok: false, reason: 'genesis-no-autofirmado' }
|
|
344
347
|
if (expectedProfileId != null && raiz.profileId !== expectedProfileId) return { ok: false, reason: 'otro-perfil' }
|
|
345
348
|
const vr = await verifySealerLink(raiz)
|
|
@@ -619,7 +622,7 @@ export async function sealActa ({ acta, privateKey, privateJwk }) {
|
|
|
619
622
|
* @param {{ data?: any, signature?: string, publickey?: string, chain?: any[], expectedProfileId?: string|null }} [args]
|
|
620
623
|
*/
|
|
621
624
|
export async function verifySignedBy (args = {}) {
|
|
622
|
-
const { data, signature, publickey, chain, expectedProfileId = null } = args
|
|
625
|
+
const { data, signature, publickey, chain, expectedProfileId = null, ns = null } = args
|
|
623
626
|
if (!data || typeof signature !== 'string' || typeof publickey !== 'string') {
|
|
624
627
|
return { ok: false, reason: 'shape' }
|
|
625
628
|
}
|
|
@@ -628,7 +631,10 @@ export async function verifySignedBy (args = {}) {
|
|
|
628
631
|
const cabeza = chain[chain.length - 1]
|
|
629
632
|
// Quien firmó tiene que ser miembro CON `sign` en el acta vigente de esa cadena. Si le
|
|
630
633
|
// quitaste el permiso, sus firmas dejan de contar en cuanto se ve un acta más nueva.
|
|
631
|
-
|
|
634
|
+
// Un SERVICIO (miembro con `cn`) solo firma dentro de su cajón: quien verifica dice cuál
|
|
635
|
+
// con `ns` (el directorio de nombres acepta lo firmado por su aparato `names`, no por
|
|
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' }
|
|
632
638
|
if (!(await verifyDeviceSig({ publickey, data, signature }))) return { ok: false, reason: 'firma-invalida' }
|
|
633
639
|
return { ok: true, profileId: c.profileId, seq: c.seq, signer: publickey }
|
|
634
640
|
}
|
|
@@ -637,8 +643,15 @@ export async function verifySealerChain (chain, { expectedProfileId = null } = {
|
|
|
637
643
|
if (!Array.isArray(chain) || !chain.length) return { ok: false, reason: 'cadena-vacia' }
|
|
638
644
|
const [raiz] = chain
|
|
639
645
|
|
|
640
|
-
|
|
641
|
-
//
|
|
646
|
+
// EL ANCLA ES LA LLAVE DEL PERFIL, NO EL NÚMERO 1 (dueño, 2026-10-14). La raíz vale si
|
|
647
|
+
// está firmada por la llave que da nombre al perfil, sea el génesis o un acta posterior.
|
|
648
|
+
// Antes se exigía `seq 1`, y eso dejaba sin cadena a toda cuenta anterior a las actas v5:
|
|
649
|
+
// su génesis no llevaba `sealerChanged`, la ventana de retención lo podó, y no hay de
|
|
650
|
+
// dónde recuperarlo. Exigir el génesis no protegía de nada: quien tenga la llave del
|
|
651
|
+
// perfil fabrica un génesis alterno igual que un acta posterior, y contra eso la capa es
|
|
652
|
+
// el registro público. Lo que sí sostiene la cadena —que cada eslabón lo firmó alguien a
|
|
653
|
+
// quien el anterior autorizaba— sigue intacto.
|
|
654
|
+
if (!Number.isInteger(raiz.seq) || raiz.seq < 1) return { ok: false, reason: 'no-empieza-en-genesis' }
|
|
642
655
|
if (raiz.sealedBy !== raiz.profileId) return { ok: false, reason: 'genesis-no-autofirmado' }
|
|
643
656
|
if (expectedProfileId != null && raiz.profileId !== expectedProfileId) return { ok: false, reason: 'otro-perfil' }
|
|
644
657
|
const vr = await verifyActa({ acta: raiz })
|
package/vault/assertion.js
CHANGED
|
@@ -191,10 +191,13 @@ export async function verifyAssertion (assertion, opts = {}) {
|
|
|
191
191
|
* quien la lleva es el servicio; si el cuerpo trae `exp`, se respeta.
|
|
192
192
|
*/
|
|
193
193
|
/**
|
|
194
|
-
*
|
|
194
|
+
* `ns`: el cajón del servicio que firma (su `cn` en el acta). Solo hace falta cuando lo
|
|
195
|
+
* firmado viene de un SERVICIO de la cuenta y no de un aparato de la persona.
|
|
196
|
+
*
|
|
197
|
+
* @param {{ data?: any, signature?: string, publickey?: string, chain?: any[], audience?: string, expectedProfileId?: string|null, ns?: string|null, now?: number, maxSkewMs?: number }} [args]
|
|
195
198
|
*/
|
|
196
199
|
export async function verifySignedFor (args = {}) {
|
|
197
|
-
const { data, signature, publickey, chain, audience, expectedProfileId = null, now = Date.now(), maxSkewMs = ASSERTION_MAX_SKEW_MS } = args
|
|
200
|
+
const { data, signature, publickey, chain, audience, expectedProfileId = null, ns = null, now = Date.now(), maxSkewMs = ASSERTION_MAX_SKEW_MS } = args
|
|
198
201
|
if (typeof audience !== 'string' || !audience.trim()) return { ok: false, reason: 'no-audience' }
|
|
199
202
|
if (!data || typeof data !== 'object') return { ok: false, reason: 'shape' }
|
|
200
203
|
if (typeof data.aud !== 'string' || !data.aud) return { ok: false, reason: 'sin-destinatario' }
|
|
@@ -207,7 +210,7 @@ export async function verifySignedFor (args = {}) {
|
|
|
207
210
|
if (!Number.isFinite(data.iat)) return { ok: false, reason: 'shape' }
|
|
208
211
|
if (data.iat > now + maxSkewMs) return { ok: false, reason: 'del-futuro' }
|
|
209
212
|
}
|
|
210
|
-
const v = await verifySignedBy({ data, signature, publickey, chain, expectedProfileId })
|
|
213
|
+
const v = await verifySignedBy({ data, signature, publickey, chain, expectedProfileId, ns })
|
|
211
214
|
if (!v.ok) return { ok: false, reason: 'firma:' + v.reason }
|
|
212
215
|
return { ok: true, profileId: v.profileId, signer: v.signer, seq: v.seq, aud: data.aud }
|
|
213
216
|
}
|
package/vault/core.js
CHANGED
|
@@ -873,7 +873,9 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
873
873
|
h.sort((a, b) => a.seq - b.seq)
|
|
874
874
|
const ventana = h.slice(-ACTA_WINDOW)
|
|
875
875
|
const enVentana = new Set(ventana.map((a) => a.seq))
|
|
876
|
-
|
|
876
|
+
// El génesis cuenta siempre como eslabón: las actas anteriores a v5 no llevaban la
|
|
877
|
+
// marca, y por eso la poda se llevó el génesis de las cuentas viejas.
|
|
878
|
+
const eslabones = h.filter((a) => (a.sealerChanged || a.seq === 1) && !enVentana.has(a.seq))
|
|
877
879
|
kv.setItem(ACTA_HISTORY_STORAGE, JSON.stringify([...eslabones, ...ventana].sort((a, b) => a.seq - b.seq)))
|
|
878
880
|
}
|
|
879
881
|
|
|
@@ -888,7 +890,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
888
890
|
// La vigente cuenta como parte del material aunque todavía no esté en la historia
|
|
889
891
|
// (ahí entra al dejar de serlo). Sin esto, una cuenta recién creada devolvía [].
|
|
890
892
|
const porSeq = new Map([...loadHistory(), cur].map((a) => [a.seq, a]))
|
|
891
|
-
const eslabones = [...porSeq.values()].filter((a) => a.sealerChanged).sort((a, b) => a.seq - b.seq)
|
|
893
|
+
const eslabones = [...porSeq.values()].filter((a) => a.sealerChanged || a.seq === 1).sort((a, b) => a.seq - b.seq)
|
|
892
894
|
// Y la actual va al final si ella misma no es un eslabón: el verificador necesita ver
|
|
893
895
|
// la cabeza para saber en qué `seq` está y quién la selló.
|
|
894
896
|
return eslabones.length && eslabones[eslabones.length - 1].seq === cur.seq ? eslabones : [...eslabones, cur]
|
|
@@ -2506,6 +2508,10 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
2506
2508
|
* Una cuenta de una sola bóveda devuelve SIEMPRE eso y nada más, y no puede quedarse
|
|
2507
2509
|
* obsoleta: su conjunto de selladores no puede cambiar, porque el único no puede
|
|
2508
2510
|
* quitarse a sí mismo y no hay otro que se lo quite.
|
|
2511
|
+
*
|
|
2512
|
+
* SIN GÉNESIS, la cadena es `[acta actual]` y vale igual mientras la actual esté firmada
|
|
2513
|
+
* por la llave del perfil: la raíz la ancla esa llave, no el número 1 (`verifySealerChain`).
|
|
2514
|
+
* Pasa en las cuentas anteriores a v5, cuyo génesis se podó sin la marca de eslabón.
|
|
2509
2515
|
*/
|
|
2510
2516
|
async sealerChain () { return sealerChain() },
|
|
2511
2517
|
|