@dotrino/identity 0.87.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 +1 -1
- package/src/index.d.ts +41 -0
- package/src/index.js +18 -0
- package/src/node.js +3 -0
- package/vault/core.js +174 -3
- package/vault/vendor/proxy-client/VERSION.txt +1 -1
- package/vault/vendor/vault/VERSION.txt +1 -1
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -167,6 +167,17 @@ 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
|
+
/**
|
|
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
|
+
*/
|
|
177
|
+
vaultApprovals (op: 'approvals' | 'approve' | 'deny', args?: { id?: string; profile?: string }): Promise<any>
|
|
178
|
+
/** Los pedidos de TODAS las cuentas de este dispositivo que aprueban, sin cambiar la activa. */
|
|
179
|
+
vaultApprovalsAll (): Promise<Array<{ profile: string; name: string; current: boolean; items: ApprovalRequest[]; error?: string }>>
|
|
180
|
+
canApproveVault (): Promise<boolean>
|
|
170
181
|
getVaultCert (): Promise<any>
|
|
171
182
|
onVault (handler: (payload: any) => void): () => void
|
|
172
183
|
// self-vault (este dispositivo ES el vault, daemon dentro del iframe)
|
|
@@ -318,3 +329,33 @@ export function signSession (args: { sid: string; s: string; by: string; origin:
|
|
|
318
329
|
export function verifySession (paper: SessionPaper, opts: { chain: any[]; expectedProfileId?: string | null; origin?: string | null; now?: number; maxSkewMs?: number }): Promise<VerifiedSession>
|
|
319
330
|
/** ¿Firmó esta sesión esto, y su papel lo cubría? */
|
|
320
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/src/index.js
CHANGED
|
@@ -463,11 +463,29 @@ export class Identity {
|
|
|
463
463
|
/**
|
|
464
464
|
* PEDIDOS DE APROBACIÓN de la bóveda (cajones con `approval`): `op` = `approvals` ·
|
|
465
465
|
* `approve` · `deny` (estos dos con `{ id }`). Requiere `vault:approve` en el cert.
|
|
466
|
+
*
|
|
467
|
+
* `args.profile` apunta a OTRA cuenta de este dispositivo (la que diga
|
|
468
|
+
* `vaultApprovalsAll`); sin él es la activa.
|
|
466
469
|
*/
|
|
467
470
|
async vaultApprovals (op, args) {
|
|
468
471
|
return this._call('vaultApprovals', { op, ...(args || {}) }, 20000)
|
|
469
472
|
}
|
|
470
473
|
|
|
474
|
+
/**
|
|
475
|
+
* LOS PEDIDOS DE TODAS TUS CUENTAS, SIN CAMBIARTE DE CUENTA.
|
|
476
|
+
*
|
|
477
|
+
* Una entrada por cuenta de este dispositivo que pueda aprobar: `{ profile, name,
|
|
478
|
+
* current, items }`, o `{ ..., error }` si a esa no se le pudo preguntar. El timbre no
|
|
479
|
+
* dice a qué cuenta llamó, así que quien enseña Pedidos tiene que mirar en todas —y
|
|
480
|
+
* hacerlo cambiando la cuenta activa significaba una recarga por cuenta, con el avatar
|
|
481
|
+
* y el icono de la app cambiando a la vista.
|
|
482
|
+
*
|
|
483
|
+
* Aprobar uno de otra cuenta: `vaultApprovals('approve', { id, profile })`.
|
|
484
|
+
*/
|
|
485
|
+
async vaultApprovalsAll () {
|
|
486
|
+
return this._call('vaultApprovalsAll', {}, 30000)
|
|
487
|
+
}
|
|
488
|
+
|
|
471
489
|
/** Registra el token de push de la app nativa (FCM/APNs) bajo la llave de este aparato. */
|
|
472
490
|
async registerPush (args) {
|
|
473
491
|
return this._call('registerPush', args || {}, 20000)
|
package/src/node.js
CHANGED
|
@@ -257,6 +257,9 @@ export class Identity {
|
|
|
257
257
|
vaultStore (method, args) { return this._h('vaultStore', { method, args }) }
|
|
258
258
|
listVaultDevices () { return this._h('listVaultDevices') }
|
|
259
259
|
getVaultCert () { return this._h('getVaultCert') }
|
|
260
|
+
vaultApprovals (op, args) { return this._h('vaultApprovals', { op, ...(args || {}) }) }
|
|
261
|
+
/** Los pedidos de TODAS las cuentas que aprueban, sin cambiar la activa. */
|
|
262
|
+
vaultApprovalsAll () { return this._h('vaultApprovalsAll') }
|
|
260
263
|
onVault (handler) { return this.on('vault', handler) }
|
|
261
264
|
// Multi-perfil por dispositivo (crear/cambiar reinicializa con el nuevo perfil activo).
|
|
262
265
|
listProfiles () { return this._h('listProfiles') }
|
package/vault/core.js
CHANGED
|
@@ -527,6 +527,102 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
527
527
|
} catch (_) { return null }
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
/**
|
|
531
|
+
* LA LLAVE DE FIRMA DE OTRA CUENTA DE ESTE DISPOSITIVO, sin abrirla ni ponerla activa.
|
|
532
|
+
*
|
|
533
|
+
* Existe para poder PREGUNTAR por los pedidos de cada cuenta sin cambiarse a ella (ver
|
|
534
|
+
* `vaultApprovalsAll`). La privada nunca se ve: en el almacén de llaves es una `CryptoKey`
|
|
535
|
+
* no extraíble y se devuelve tal cual, para firmar y nada más.
|
|
536
|
+
*
|
|
537
|
+
* Tres cosas que NO se hacen aquí, y las tres importan:
|
|
538
|
+
*
|
|
539
|
+
* · **No se mueve `currentPid`.** Abrir otra cuenta «un momento» pisa la identidad en
|
|
540
|
+
* memoria y además persiste cuál es la activa: cualquier otra llamada que caiga en medio
|
|
541
|
+
* se atendería con la cuenta equivocada, y una recarga a mitad te deja en otra cuenta.
|
|
542
|
+
* · **No se genera ninguna llave.** Si no está, se dice. Un `catch` que cae a `generateKey`
|
|
543
|
+
* le cambiaría la identidad a esa cuenta y la dejaría fuera de su propio perfil para
|
|
544
|
+
* siempre — el mismo fallo que ya está avisado en `loadOrCreatePair`.
|
|
545
|
+
* · **No se abre lo sellado.** Una cuenta del camino legado guarda su privada sellada bajo
|
|
546
|
+
* su contraseña; sin la frase no hay con qué, y se contesta `profile-locked` en vez de
|
|
547
|
+
* fingir que no existe.
|
|
548
|
+
*/
|
|
549
|
+
async function signerForProfile (pid) {
|
|
550
|
+
const nombre = KEY_STORAGE.replace(/^dotrino\.identity\./, `dotrino.identity.p.${pid}.`)
|
|
551
|
+
if (keyStore) {
|
|
552
|
+
const guardado = await keyStore.get(nombre).catch(() => null)
|
|
553
|
+
if (guardado?.privateKey && guardado?.publicJwk) {
|
|
554
|
+
return { publickey: JSON.stringify(guardado.publicJwk), privateKey: guardado.privateKey }
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const raw = rawKv.getItem(nombre)
|
|
558
|
+
if (raw) {
|
|
559
|
+
const g = JSON.parse(raw)
|
|
560
|
+
if (g?.sealed) throw Object.assign(new Error('that profile is locked: its key is sealed under its password'), { code: 'profile-locked' })
|
|
561
|
+
if (g?.privateJwk && g?.publicJwk) return { publickey: JSON.stringify(g.publicJwk), privateJwk: g.privateJwk }
|
|
562
|
+
}
|
|
563
|
+
throw Object.assign(new Error('no signing key stored for that profile'), { code: 'no-key' })
|
|
564
|
+
}
|
|
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
|
+
|
|
530
626
|
/** La cuenta de este dispositivo que YA está emparejada con la bóveda `master`, si la hay. */
|
|
531
627
|
const profilePairedWith = (master) => {
|
|
532
628
|
if (!master) return null
|
|
@@ -575,6 +671,25 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
575
671
|
if (e && /\brevoked\b/.test(e.message || '')) emitVault({ phase: 'rejected', reason: e.message })
|
|
576
672
|
throw e
|
|
577
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* APROBAR O DENEGAR un pedido de OTRA cuenta de este dispositivo.
|
|
676
|
+
*
|
|
677
|
+
* Es el par de `vaultApprovalsAll`: si la pantalla enseña los pedidos de las tres cuentas,
|
|
678
|
+
* el botón «Aprobar» tiene que funcionar en las tres — si no, seguirías teniendo que
|
|
679
|
+
* cambiarte, que es lo que esto vino a quitar. Firma con la llave de ESA cuenta y no toca
|
|
680
|
+
* nada de la activa.
|
|
681
|
+
*
|
|
682
|
+
* Solo lo suyo: sin cert o sin `vault:approve` en ese cert, no hay nada que hacer aquí.
|
|
683
|
+
*/
|
|
684
|
+
async function approvalsDeOtroPerfil (pid, { op, id } = {}) {
|
|
685
|
+
if (!loadProfiles().some((p) => p.id === pid)) throw Object.assign(new Error('that profile does not exist on this device'), { code: 'no-profile' })
|
|
686
|
+
const v = vaultCertOf(pid)
|
|
687
|
+
if (!v?.cert) throw Object.assign(new Error('that profile is not paired with a vault'), { code: 'not-paired' })
|
|
688
|
+
if (!(v.cert.scope || []).includes('vault:approve')) throw Object.assign(new Error('that profile does not approve requests'), { code: 'no-approve' })
|
|
689
|
+
const device = await signerForProfile(pid)
|
|
690
|
+
return remoteApproval({ master: v.master, proxy: v.proxy, device, cert: v.cert, op, id })
|
|
691
|
+
}
|
|
692
|
+
|
|
578
693
|
/** Id estable y corto de una llave de cifrado: con esto se indexan las envolturas. */
|
|
579
694
|
const encKeyId = async (encPub) => (await pubkeyIdOf(encPub)).slice(0, 16)
|
|
580
695
|
|
|
@@ -2501,13 +2616,69 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
2501
2616
|
* PEDIDOS DE APROBACIÓN: lo que le toca al teléfono cuando un cajón de la bóveda exige
|
|
2502
2617
|
* el visto bueno por uso. `op`: `approvals` (listar) · `approve` · `deny` (con `id`).
|
|
2503
2618
|
* Requiere `vault:approve` en el cert, que se concede a mano (`caps <ID> +aprueba`).
|
|
2619
|
+
*
|
|
2620
|
+
* `profile` apunta a OTRA cuenta de este mismo dispositivo (ver `vaultApprovalsAll`):
|
|
2621
|
+
* sin él es la activa, como siempre.
|
|
2504
2622
|
*/
|
|
2505
|
-
async vaultApprovals ({ op, id } = {}) {
|
|
2623
|
+
async vaultApprovals ({ op, id, profile = null } = {}) {
|
|
2624
|
+
if (profile && profile !== currentPid) return approvalsDeOtroPerfil(profile, { op, id })
|
|
2506
2625
|
const v = loadVaultCert(); const device = loadVaultDevice()
|
|
2507
2626
|
if (!v?.cert || !device) throw new Error('this device is not paired with a vault')
|
|
2508
2627
|
maybeRenewVaultCert()
|
|
2509
|
-
try {
|
|
2510
|
-
|
|
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) }
|
|
2635
|
+
},
|
|
2636
|
+
|
|
2637
|
+
/**
|
|
2638
|
+
* LOS PEDIDOS DE TODAS TUS CUENTAS, SIN CAMBIARTE DE CUENTA.
|
|
2639
|
+
*
|
|
2640
|
+
* El timbre del teléfono no dice a qué perfil llamó —viaja por FCM, o sea por Google, y
|
|
2641
|
+
* ahí no se mete nada que identifique al dueño—, así que la pantalla de Pedidos tiene
|
|
2642
|
+
* que mirar en todos los que aprueban. Antes lo hacía cambiando el perfil activo y
|
|
2643
|
+
* recargando la página una vez por cuenta: funcionaba y era insufrible, porque el avatar
|
|
2644
|
+
* y el icono de la app cambiaban dos o tres veces por abrir la pantalla (dueño,
|
|
2645
|
+
* 2026-09-10: «rotan los perfiles, cambia el icono y es molesto»).
|
|
2646
|
+
*
|
|
2647
|
+
* Y era innecesario: **este iframe tiene las llaves de todos los perfiles**. Cada cuenta
|
|
2648
|
+
* guarda la suya en el mismo almacén, bajo su propio nombre, y son `CryptoKey` NO
|
|
2649
|
+
* EXTRAÍBLES: se puede firmar con ellas sin verlas y sin tocar cuál es la activa. Eso es
|
|
2650
|
+
* justo lo que hace falta, y es lo único que se hace aquí.
|
|
2651
|
+
*
|
|
2652
|
+
* Lo que NO hace, a propósito: no renueva certificados ajenos ni borra enlaces ajenos
|
|
2653
|
+
* (eso escribe, y escribir en otra cuenta desde la pantalla de otra es pedir un lío).
|
|
2654
|
+
* Mirar es de solo lectura; si el papel de una cuenta está desfasado, se dice y punto.
|
|
2655
|
+
*
|
|
2656
|
+
* @returns {Promise<Array<{ profile, name, items, error? }>>} una entrada por cuenta que
|
|
2657
|
+
* aprueba — con sus pedidos, o con el motivo por el que no se pudo preguntar.
|
|
2658
|
+
*/
|
|
2659
|
+
async vaultApprovalsAll () {
|
|
2660
|
+
// La activa sí se pone al día: es la única en la que esta pantalla puede escribir.
|
|
2661
|
+
try { maybeRenewVaultCert() } catch (_) {}
|
|
2662
|
+
const perfiles = loadProfiles()
|
|
2663
|
+
const salida = await Promise.all(perfiles.map(async (p) => {
|
|
2664
|
+
const v = p.id === currentPid ? loadVaultCert() : vaultCertOf(p.id)
|
|
2665
|
+
if (!v?.cert) return null // sin bóveda: no es asunto suyo
|
|
2666
|
+
if (!(v.cert.scope || []).includes('vault:approve')) return null // no aprueba: tampoco
|
|
2667
|
+
const base = { profile: p.id, name: p.name || '', current: p.id === currentPid }
|
|
2668
|
+
let device = null
|
|
2669
|
+
try { device = p.id === currentPid ? loadVaultDevice() : await signerForProfile(p.id) } catch (e) {
|
|
2670
|
+
return { ...base, items: [], error: e?.code || 'no-key' }
|
|
2671
|
+
}
|
|
2672
|
+
if (!device) return { ...base, items: [], error: 'no-key' }
|
|
2673
|
+
try {
|
|
2674
|
+
const r = await remoteApproval({ master: v.master, proxy: v.proxy, device, cert: v.cert, op: 'approvals' })
|
|
2675
|
+
const items = Array.isArray(r?.items) ? await abrirContextos(r.items, p.id) : []
|
|
2676
|
+
return { ...base, items }
|
|
2677
|
+
} catch (e) {
|
|
2678
|
+
return { ...base, items: [], error: e?.code || e?.message || 'error' }
|
|
2679
|
+
}
|
|
2680
|
+
}))
|
|
2681
|
+
return salida.filter(Boolean)
|
|
2511
2682
|
},
|
|
2512
2683
|
|
|
2513
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
|