@dotrino/identity 0.88.0 → 0.89.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
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
|
|
@@ -2565,8 +2625,13 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
2565
2625
|
const v = loadVaultCert(); const device = loadVaultDevice()
|
|
2566
2626
|
if (!v?.cert || !device) throw new Error('this device is not paired with a vault')
|
|
2567
2627
|
maybeRenewVaultCert()
|
|
2568
|
-
try {
|
|
2569
|
-
|
|
2628
|
+
try {
|
|
2629
|
+
const r = await remoteApproval({ master: v.master, proxy: v.proxy, device, cert: v.cert, op, id, onRevoked: wipeVaultLink })
|
|
2630
|
+
// El comando de cada pedido viene sellado a este aparato: se abre aquí, que es
|
|
2631
|
+
// donde está la llave, y sale ya legible.
|
|
2632
|
+
if (Array.isArray(r?.items)) return { ...r, items: await abrirContextos(r.items, currentPid) }
|
|
2633
|
+
return r
|
|
2634
|
+
} catch (e) { return handleVaultError(e) }
|
|
2570
2635
|
},
|
|
2571
2636
|
|
|
2572
2637
|
/**
|
|
@@ -2607,7 +2672,8 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
2607
2672
|
if (!device) return { ...base, items: [], error: 'no-key' }
|
|
2608
2673
|
try {
|
|
2609
2674
|
const r = await remoteApproval({ master: v.master, proxy: v.proxy, device, cert: v.cert, op: 'approvals' })
|
|
2610
|
-
|
|
2675
|
+
const items = Array.isArray(r?.items) ? await abrirContextos(r.items, p.id) : []
|
|
2676
|
+
return { ...base, items }
|
|
2611
2677
|
} catch (e) {
|
|
2612
2678
|
return { ...base, items: [], error: e?.code || e?.message || 'error' }
|
|
2613
2679
|
}
|
|
@@ -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
|