@dotrino/identity 0.88.0 → 0.89.1
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
package/src/index.d.ts
CHANGED
|
@@ -167,10 +167,16 @@ export class Identity {
|
|
|
167
167
|
vaultSign (payload: any): Promise<{ signature: string; publickey: string }>
|
|
168
168
|
vaultStore (method: string, args?: any): Promise<any>
|
|
169
169
|
listVaultDevices (): Promise<{ devices: any[]; revoked: any[] }>
|
|
170
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Pedidos de aprobación de la cuenta activa (o de otra, con `profile`).
|
|
172
|
+
*
|
|
173
|
+
* Cada pedido trae `ctx` —qué comando está pidiendo las claves y desde qué carpeta— ya
|
|
174
|
+
* abierto: viaja sellado a la llave de cifrado de este aparato y se descifra aquí dentro.
|
|
175
|
+
* `ctxError` si no se pudo abrir; sin ninguno de los dos, el pedido no dijo qué corría.
|
|
176
|
+
*/
|
|
171
177
|
vaultApprovals (op: 'approvals' | 'approve' | 'deny', args?: { id?: string; profile?: string }): Promise<any>
|
|
172
178
|
/** Los pedidos de TODAS las cuentas de este dispositivo que aprueban, sin cambiar la activa. */
|
|
173
|
-
vaultApprovalsAll (): Promise<Array<{ profile: string; name: string; current: boolean; items:
|
|
179
|
+
vaultApprovalsAll (): Promise<Array<{ profile: string; name: string; current: boolean; items: ApprovalRequest[]; error?: string }>>
|
|
174
180
|
canApproveVault (): Promise<boolean>
|
|
175
181
|
getVaultCert (): Promise<any>
|
|
176
182
|
onVault (handler: (payload: any) => void): () => void
|
|
@@ -323,3 +329,33 @@ export function signSession (args: { sid: string; s: string; by: string; origin:
|
|
|
323
329
|
export function verifySession (paper: SessionPaper, opts: { chain: any[]; expectedProfileId?: string | null; origin?: string | null; now?: number; maxSkewMs?: number }): Promise<VerifiedSession>
|
|
324
330
|
/** ¿Firmó esta sesión esto, y su papel lo cubría? */
|
|
325
331
|
export function verifySessionSigned (args: { data: any; signature: string; session: SessionPaper; chain: any[]; scope?: SessionScope | null; origin?: string | null; expectedProfileId?: string | null; now?: number }): Promise<VerifiedSession>
|
|
332
|
+
|
|
333
|
+
/** Un pedido de aprobación tal y como lo ve la pantalla que dice que sí o que no. */
|
|
334
|
+
export interface ApprovalRequest {
|
|
335
|
+
id: string
|
|
336
|
+
ns: string
|
|
337
|
+
deviceId: string | null
|
|
338
|
+
label: string
|
|
339
|
+
ts: number
|
|
340
|
+
exp: number
|
|
341
|
+
/** Qué comando está pidiendo las claves, ya descifrado. `null`/ausente si no lo dijo. */
|
|
342
|
+
ctx?: ProcessContext | null
|
|
343
|
+
/** Por qué no se pudo abrir el comando (`no-key`, `profile-locked`, `cannot-open`). */
|
|
344
|
+
ctxError?: string
|
|
345
|
+
/** La bóveda no pudo sellarlo para este aparato (`no-enc-key`, `seal-failed`). */
|
|
346
|
+
ctxSealed?: boolean
|
|
347
|
+
ctxReason?: string
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Qué proceso pide, y si lo comprobó el kernel (`proc`) o solo lo dice él (`declared`). */
|
|
351
|
+
export interface ProcessContext {
|
|
352
|
+
pid: number | null
|
|
353
|
+
uid: number | null
|
|
354
|
+
exe: string
|
|
355
|
+
cwd: string
|
|
356
|
+
argv: string[]
|
|
357
|
+
truncated: boolean
|
|
358
|
+
user: string
|
|
359
|
+
host: string
|
|
360
|
+
verified: 'proc' | 'declared'
|
|
361
|
+
}
|
package/vault/core.js
CHANGED
|
@@ -563,6 +563,66 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
563
563
|
throw Object.assign(new Error('no signing key stored for that profile'), { code: 'no-key' })
|
|
564
564
|
}
|
|
565
565
|
|
|
566
|
+
/**
|
|
567
|
+
* LA LLAVE DE CIFRADO DE OTRA CUENTA DE ESTE DISPOSITIVO, sin abrirla ni ponerla activa.
|
|
568
|
+
*
|
|
569
|
+
* El par de `signerForProfile`, y por el mismo motivo: la pantalla de Pedidos enseña los
|
|
570
|
+
* de TODAS las cuentas a la vez, y desde 2026-09-11 cada pedido trae **qué comando está
|
|
571
|
+
* pidiendo las claves** dentro de un sobre cerrado a la llave de ESA cuenta. Sin esto, los
|
|
572
|
+
* pedidos de las demás cuentas se verían sin comando — que es justo el dato por el que se
|
|
573
|
+
* mira la pantalla.
|
|
574
|
+
*
|
|
575
|
+
* Mismas tres reglas que allí: no se mueve `currentPid`, no se genera ninguna llave (si no
|
|
576
|
+
* está, se dice) y una cuenta sellada bajo su contraseña se contesta `profile-locked`.
|
|
577
|
+
*/
|
|
578
|
+
async function encKeyForProfile (pid) {
|
|
579
|
+
if (pid === currentPid) return encKeypair?.privateKey || null
|
|
580
|
+
const { algo, privUses } = ALGO_OF.enc
|
|
581
|
+
const nombre = ENC_KEY_STORAGE.replace(/^dotrino\.identity\./, `dotrino.identity.p.${pid}.`)
|
|
582
|
+
if (keyStore) {
|
|
583
|
+
const guardado = await keyStore.get(nombre).catch(() => null)
|
|
584
|
+
if (guardado?.privateKey) return guardado.privateKey
|
|
585
|
+
}
|
|
586
|
+
const raw = rawKv.getItem(nombre)
|
|
587
|
+
if (raw) {
|
|
588
|
+
const g = JSON.parse(raw)
|
|
589
|
+
if (g?.sealed) throw Object.assign(new Error('that profile is locked: its encryption key is sealed under its password'), { code: 'profile-locked' })
|
|
590
|
+
if (g?.privateJwk) return crypto.subtle.importKey('jwk', g.privateJwk, algo, true, privUses)
|
|
591
|
+
}
|
|
592
|
+
throw Object.assign(new Error('no encryption key stored for that profile'), { code: 'no-key' })
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* ABRE EL COMANDO DE CADA PEDIDO, aquí dentro, donde están las llaves.
|
|
597
|
+
*
|
|
598
|
+
* La bóveda manda el comando y el path sellados a la llave de cifrado del aparato que
|
|
599
|
+
* pregunta (no los manda en claro: el camino hasta aquí es el proxio, que no cifra). Se
|
|
600
|
+
* abren en el iframe y salen a la página ya legibles: la página no ve ninguna llave, y el
|
|
601
|
+
* único tramo en claro es el `postMessage` entre dos ventanas del mismo navegador.
|
|
602
|
+
*
|
|
603
|
+
* Lo que no se puede abrir se dice (`ctxError`) en vez de quedarse como un pedido sin
|
|
604
|
+
* comando: son cosas distintas y en la pantalla hay que poder distinguirlas.
|
|
605
|
+
*/
|
|
606
|
+
async function abrirContextos (items, pid) {
|
|
607
|
+
if (!Array.isArray(items) || !items.length) return items
|
|
608
|
+
let priv = null
|
|
609
|
+
let fallo = null
|
|
610
|
+
try { priv = await encKeyForProfile(pid) } catch (e) { fallo = e?.code || 'no-key' }
|
|
611
|
+
const salida = []
|
|
612
|
+
for (const raw of items) {
|
|
613
|
+
const { ctxWrap, ctxEnvelope, ...it } = raw || {}
|
|
614
|
+
if (!ctxWrap || !ctxEnvelope) { salida.push(it); continue }
|
|
615
|
+
if (!priv) { salida.push({ ...it, ctxError: fallo || 'no-key' }); continue }
|
|
616
|
+
try {
|
|
617
|
+
const cek = await Content.openWrap({ wrap: ctxWrap, myEncPrivateKey: priv })
|
|
618
|
+
salida.push({ ...it, ctx: JSON.parse(await Content.decryptWithCek({ cek, envelope: ctxEnvelope })) })
|
|
619
|
+
} catch (e) {
|
|
620
|
+
salida.push({ ...it, ctxError: e?.code || 'cannot-open' })
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return salida
|
|
624
|
+
}
|
|
625
|
+
|
|
566
626
|
/** La cuenta de este dispositivo que YA está emparejada con la bóveda `master`, si la hay. */
|
|
567
627
|
const profilePairedWith = (master) => {
|
|
568
628
|
if (!master) return null
|
|
@@ -627,7 +687,12 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
627
687
|
if (!v?.cert) throw Object.assign(new Error('that profile is not paired with a vault'), { code: 'not-paired' })
|
|
628
688
|
if (!(v.cert.scope || []).includes('vault:approve')) throw Object.assign(new Error('that profile does not approve requests'), { code: 'no-approve' })
|
|
629
689
|
const device = await signerForProfile(pid)
|
|
630
|
-
|
|
690
|
+
const r = await remoteApproval({ master: v.master, proxy: v.proxy, device, cert: v.cert, op, id })
|
|
691
|
+
// Y EL COMANDO SE ABRE TAMBIÉN AQUÍ, con la llave de ESA cuenta. Sin esto los pedidos de
|
|
692
|
+
// las demás cuentas se veían sin comando —el dato por el que se mira la pantalla— y el
|
|
693
|
+
// fallo era mudo: llegaban con su sobre cerrado y nadie lo abría.
|
|
694
|
+
if (Array.isArray(r?.items)) return { ...r, items: await abrirContextos(r.items, pid) }
|
|
695
|
+
return r
|
|
631
696
|
}
|
|
632
697
|
|
|
633
698
|
/** Id estable y corto de una llave de cifrado: con esto se indexan las envolturas. */
|
|
@@ -2565,8 +2630,13 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
2565
2630
|
const v = loadVaultCert(); const device = loadVaultDevice()
|
|
2566
2631
|
if (!v?.cert || !device) throw new Error('this device is not paired with a vault')
|
|
2567
2632
|
maybeRenewVaultCert()
|
|
2568
|
-
try {
|
|
2569
|
-
|
|
2633
|
+
try {
|
|
2634
|
+
const r = await remoteApproval({ master: v.master, proxy: v.proxy, device, cert: v.cert, op, id, onRevoked: wipeVaultLink })
|
|
2635
|
+
// El comando de cada pedido viene sellado a este aparato: se abre aquí, que es
|
|
2636
|
+
// donde está la llave, y sale ya legible.
|
|
2637
|
+
if (Array.isArray(r?.items)) return { ...r, items: await abrirContextos(r.items, currentPid) }
|
|
2638
|
+
return r
|
|
2639
|
+
} catch (e) { return handleVaultError(e) }
|
|
2570
2640
|
},
|
|
2571
2641
|
|
|
2572
2642
|
/**
|
|
@@ -2607,7 +2677,8 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
2607
2677
|
if (!device) return { ...base, items: [], error: 'no-key' }
|
|
2608
2678
|
try {
|
|
2609
2679
|
const r = await remoteApproval({ master: v.master, proxy: v.proxy, device, cert: v.cert, op: 'approvals' })
|
|
2610
|
-
|
|
2680
|
+
const items = Array.isArray(r?.items) ? await abrirContextos(r.items, p.id) : []
|
|
2681
|
+
return { ...base, items }
|
|
2611
2682
|
} catch (e) {
|
|
2612
2683
|
return { ...base, items: [], error: e?.code || e?.message || 'error' }
|
|
2613
2684
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/proxy-client@0.
|
|
1
|
+
Copia vendorizada de @dotrino/proxy-client@0.19.0 (dotrino-proxy-client/src/{index,client,signature,canonical,sealing,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.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/vault@0.
|
|
1
|
+
Copia vendorizada de @dotrino/vault@0.63.0 (dotrino-vault/lib/src/{index,enroll,protocol}.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
|
index.js importa ./enroll.js y ./protocol.js (relativos, van en esta misma copia),
|
|
4
4
|
@dotrino/identity/{capabilities,acta} (= ../../{capabilities,acta}.js) y
|