@dotrino/identity 0.22.1 → 0.24.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 +5 -2
- package/src/index.js +49 -5
- package/src/node.js +12 -0
- package/vault/acta.js +315 -0
- package/vault/core.js +286 -22
- package/vault/remote.js +66 -38
- package/vault/vault.js +5 -1
- package/vault/vendor/vault/VERSION.txt +6 -5
- package/vault/vendor/vault/enroll.js +269 -0
- package/vault/vendor/vault/index.js +35 -120
package/vault/core.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { signDelegationWith, MAX_DELEGATION_MS, DEFAULT_DELEGATION_MS } from './capabilities.js'
|
|
22
|
+
import * as Acta from './acta.js'
|
|
22
23
|
import { enrollDevice as remoteEnroll, requestSign as remoteSign, requestStore as remoteStore, requestDevices as remoteDevices, requestRenew as remoteRenew } from './remote.js'
|
|
23
24
|
|
|
24
25
|
export const KEY_STORAGE = 'dotrino.identity.keypair'
|
|
@@ -29,6 +30,8 @@ export const DELEGATIONS_STORAGE = 'dotrino.identity.delegations' // caps emit
|
|
|
29
30
|
export const REVOCATIONS_STORAGE = 'dotrino.identity.revocations' // nonces revocados
|
|
30
31
|
export const VAULT_DEVICE_STORAGE = 'dotrino.identity.vault.device' // sub-clave D de ESTE dispositivo (custodia en el iframe)
|
|
31
32
|
export const VAULT_CERT_STORAGE = 'dotrino.identity.vault.cert' // { cert, master, proxy, deviceId, pairedAt }
|
|
33
|
+
export const ACTA_STORAGE = 'dotrino.identity.acta' // acta de perfil vigente (quién es del perfil y qué puede)
|
|
34
|
+
export const RENOUNCE_STORAGE = 'dotrino.identity.renounced' // renuncias propias aún no absorbidas por el master
|
|
32
35
|
// Multi-perfil por dispositivo: lista de perfiles + el activo. Cada perfil tiene su propio
|
|
33
36
|
// namespace `dotrino.identity.p.<id>.<suffix>` para TODAS las claves de arriba (keypair, me, etc.).
|
|
34
37
|
export const PROFILES_STORAGE = 'dotrino.identity.profiles' // [{ id, name, pubkey }]
|
|
@@ -249,23 +252,69 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
249
252
|
// ----- delegaciones de capacidad emitidas + revocaciones (kv-backed) -----
|
|
250
253
|
|
|
251
254
|
function loadJson (key) { try { return JSON.parse(kv.getItem(key) || '{}') || {} } catch (_) { return {} } }
|
|
252
|
-
|
|
255
|
+
|
|
256
|
+
// PODA (los dos registros crecían para siempre): la renovación automática firma un cert
|
|
257
|
+
// nuevo cada 30 días, así que sin podar cada dispositivo dejaba 12 entradas muertas al año.
|
|
258
|
+
// Se tira lo que YA NO PUEDE SERVIR, nunca lo vivo:
|
|
259
|
+
// · delegación → cuando su `exp` ya pasó (un cert vencido no autoriza nada).
|
|
260
|
+
// OJO: no se poda «la anterior del mismo dispositivo» al renovar, porque el cert
|
|
261
|
+
// viejo SIGUE VIGENTE hasta su exp y hay que poder revocarlo si te roban el aparato.
|
|
262
|
+
// · revocación → 30 días después de revocar: para entonces el cert al que apunta está
|
|
263
|
+
// vencido seguro (el tope duro de vida es `MAX_DELEGATION_MS`, y exp ≤ iat + 30 días
|
|
264
|
+
// ≤ revokedAt + 30 días), y un cert vencido ya falla por `expired` sin mirar la lista.
|
|
265
|
+
const DELEGATION_MAX_LIFE_MS = 30 * 24 * 60 * 60 * 1000 // espejo de MAX_DELEGATION_MS (capabilities.js)
|
|
266
|
+
|
|
267
|
+
function loadDelegations () {
|
|
268
|
+
const o = loadJson(DELEGATIONS_STORAGE)
|
|
269
|
+
const now = Date.now()
|
|
270
|
+
let changed = false
|
|
271
|
+
for (const k of Object.keys(o)) {
|
|
272
|
+
const exp = o[k]?.exp
|
|
273
|
+
if (typeof exp === 'number' && exp < now) { delete o[k]; changed = true }
|
|
274
|
+
}
|
|
275
|
+
if (changed) kv.setItem(DELEGATIONS_STORAGE, JSON.stringify(o))
|
|
276
|
+
return o
|
|
277
|
+
}
|
|
253
278
|
const saveDelegations = (o) => kv.setItem(DELEGATIONS_STORAGE, JSON.stringify(o))
|
|
254
|
-
|
|
279
|
+
|
|
280
|
+
function loadRevocations () {
|
|
281
|
+
const o = loadJson(REVOCATIONS_STORAGE)
|
|
282
|
+
const now = Date.now()
|
|
283
|
+
let changed = false
|
|
284
|
+
for (const k of Object.keys(o)) {
|
|
285
|
+
const at = o[k]
|
|
286
|
+
if (typeof at === 'number' && now - at > DELEGATION_MAX_LIFE_MS) { delete o[k]; changed = true }
|
|
287
|
+
}
|
|
288
|
+
if (changed) kv.setItem(REVOCATIONS_STORAGE, JSON.stringify(o))
|
|
289
|
+
return o
|
|
290
|
+
}
|
|
255
291
|
const saveRevocations = (o) => kv.setItem(REVOCATIONS_STORAGE, JSON.stringify(o))
|
|
256
292
|
|
|
257
293
|
// ----- emparejamiento con el vault del usuario (este dispositivo enrolado) -----
|
|
258
|
-
// Canal de eventos 'vault' (p.ej. el
|
|
294
|
+
// Canal de eventos 'vault' (p.ej. el código a tipear durante el emparejamiento).
|
|
259
295
|
const vaultListeners = new Set()
|
|
260
296
|
const emitVault = (p) => { for (const fn of vaultListeners) { try { fn(p) } catch (_) {} } }
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* BORRADO por revocación. Solo lo dispara un `vault.revoked` FIRMADO por la maestra
|
|
300
|
+
* pineada (lo verifica `remote.js` antes de llamar aquí). Emite 'revoked' →
|
|
301
|
+
* `@dotrino/store` borra el store de ESTE perfil (los demás quedan intactos).
|
|
302
|
+
*/
|
|
303
|
+
const wipeVaultLink = () => {
|
|
304
|
+
try { kv.removeItem(VAULT_CERT_STORAGE); kv.removeItem(VAULT_DEVICE_STORAGE) } catch (_) {}
|
|
305
|
+
emitVault({ phase: 'revoked' })
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* El vault RECHAZÓ una petición diciendo «revocado». Ese mensaje NO va firmado: lo
|
|
310
|
+
* puede falsificar cualquiera que conozca la pubkey de este dispositivo, así que
|
|
311
|
+
* **jamás borra nada** (sería un wipe-DoS: destruir datos ajenos con un mensaje suelto,
|
|
312
|
+
* prohibido por `dotrino-vault/docs/pairing-protocol.md §2.3`). Lo único que hacemos es
|
|
313
|
+
* DEGRADAR: avisar a la app de que la bóveda nos está rechazando, y que sea el usuario
|
|
314
|
+
* quien decida. El borrado real llega por `vault.revoked` firmado (ver `wipeVaultLink`).
|
|
315
|
+
*/
|
|
264
316
|
const handleVaultError = (e) => {
|
|
265
|
-
if (e && /\brevoked\b/.test(e.message || '')) {
|
|
266
|
-
try { kv.removeItem(VAULT_CERT_STORAGE); kv.removeItem(VAULT_DEVICE_STORAGE) } catch (_) {}
|
|
267
|
-
emitVault({ phase: 'revoked' })
|
|
268
|
-
}
|
|
317
|
+
if (e && /\brevoked\b/.test(e.message || '')) emitVault({ phase: 'rejected', reason: e.message })
|
|
269
318
|
throw e
|
|
270
319
|
}
|
|
271
320
|
const loadVaultCert = () => { try { return JSON.parse(kv.getItem(VAULT_CERT_STORAGE) || 'null') } catch (_) { return null } }
|
|
@@ -289,6 +338,86 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
289
338
|
} catch (_) { return null }
|
|
290
339
|
}
|
|
291
340
|
|
|
341
|
+
// ----- ACTA DE PERFIL: qué llaves son de este perfil y qué puede hacer cada una -----
|
|
342
|
+
// Diseño en `dotrino-vault/docs/acta-de-perfil.md`. Aquí solo se guarda, se lee y se
|
|
343
|
+
// sella; las reglas (sellador único, seq/prev, no dejar el perfil sin firmante) viven en
|
|
344
|
+
// `acta.js`, que es puro y está probado aparte.
|
|
345
|
+
const loadActa = () => { try { return JSON.parse(kv.getItem(ACTA_STORAGE) || 'null') } catch (_) { return null } }
|
|
346
|
+
const saveActa = (a) => kv.setItem(ACTA_STORAGE, JSON.stringify(a))
|
|
347
|
+
const loadRenounces = () => { try { return JSON.parse(kv.getItem(RENOUNCE_STORAGE) || '[]') || [] } catch (_) { return [] } }
|
|
348
|
+
const saveRenounces = (l) => kv.setItem(RENOUNCE_STORAGE, JSON.stringify(l))
|
|
349
|
+
|
|
350
|
+
/** ¿Es ESTE dispositivo el master (el único que puede sellar)? */
|
|
351
|
+
const amMaster = () => loadActa()?.sealer === publickeyJwkStr
|
|
352
|
+
|
|
353
|
+
/** Sella con la llave del perfil (CryptoKey, puede ser no extractable). */
|
|
354
|
+
const seal = (acta) => Acta.sealActa({ acta, privateKey: keypair.privateKey })
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Aplica cambios, sella y guarda. Solo funciona si este dispositivo es el master: es la
|
|
358
|
+
* regla 1 del modelo, y `applyChanges` la vuelve a comprobar por su cuenta.
|
|
359
|
+
*/
|
|
360
|
+
async function sealChanges (changes) {
|
|
361
|
+
const acta = loadActa()
|
|
362
|
+
if (!acta) throw new Error('este perfil todavía no tiene acta')
|
|
363
|
+
const next = await Acta.applyChanges(acta, changes, { by: publickeyJwkStr })
|
|
364
|
+
const sealed = await seal(next)
|
|
365
|
+
saveActa(sealed)
|
|
366
|
+
emitVault({ phase: 'acta', seq: sealed.seq, sealer: sealed.sealer })
|
|
367
|
+
return sealed
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Si el perfil todavía no tiene acta, la crea: un miembro (esta llave), que es el master,
|
|
372
|
+
* con todas las capacidades. `profileId` = la pubkey de este perfil → el perfil se llama
|
|
373
|
+
* como la identidad que el usuario ya tenía, así que no hay nada que migrar.
|
|
374
|
+
*/
|
|
375
|
+
async function ensureActa (label = '') {
|
|
376
|
+
if (loadActa()) return loadActa()
|
|
377
|
+
const acta = await seal(Acta.genesisActa({
|
|
378
|
+
pub: publickeyJwkStr, encPub: encPublickeyJwkStr, label: label || me?.nickname || ''
|
|
379
|
+
}))
|
|
380
|
+
saveActa(acta)
|
|
381
|
+
return acta
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* UNIRSE a otro perfil (el de la bóveda a la que te acabas de conectar). No es adoptar
|
|
386
|
+
* una versión nueva de TU acta: es cambiar de perfil, así que solo procede si aquí no
|
|
387
|
+
* hay nada que perder — es decir, si este dispositivo es el único miembro del suyo.
|
|
388
|
+
*
|
|
389
|
+
* Si ya tienes otros dispositivos, los dos lados tienen master y hay que ELEGIR cuál
|
|
390
|
+
* manda (§2.4.3): eso es una decisión del dueño, no un efecto colateral de escanear un
|
|
391
|
+
* código, así que se devuelve el conflicto para que lo resuelva la consola.
|
|
392
|
+
*/
|
|
393
|
+
async function joinProfile (candidate) {
|
|
394
|
+
const v = await Acta.verifyActa({ acta: candidate })
|
|
395
|
+
if (!v.ok) return { joined: false, reason: 'acta-invalida:' + v.reason }
|
|
396
|
+
if (!candidate.members.some((m) => m.pub === publickeyJwkStr)) {
|
|
397
|
+
return { joined: false, reason: 'no-soy-miembro' }
|
|
398
|
+
}
|
|
399
|
+
const current = loadActa()
|
|
400
|
+
if (current && current.profileId !== candidate.profileId && current.members.length > 1) {
|
|
401
|
+
return { joined: false, reason: 'ya-tienes-perfil-propio', members: current.members.length }
|
|
402
|
+
}
|
|
403
|
+
saveActa(candidate)
|
|
404
|
+
emitVault({ phase: 'acta', seq: candidate.seq, sealer: candidate.sealer, joined: true })
|
|
405
|
+
return { joined: true, profileId: candidate.profileId, seq: candidate.seq }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Adopta un acta que llega de otro miembro, si gana según §2.4.1 (seq mayor que encadene,
|
|
410
|
+
* o el traspaso a igual seq). Nunca retrocede.
|
|
411
|
+
*/
|
|
412
|
+
async function adoptActa (candidate) {
|
|
413
|
+
const current = loadActa()
|
|
414
|
+
const r = await Acta.canAdopt({ candidate, current })
|
|
415
|
+
if (!r.adopt) return { adopted: false, reason: r.reason, seq: current?.seq ?? null }
|
|
416
|
+
saveActa(candidate)
|
|
417
|
+
emitVault({ phase: 'acta', seq: candidate.seq, sealer: candidate.sealer, adopted: r.reason })
|
|
418
|
+
return { adopted: true, reason: r.reason, seq: candidate.seq }
|
|
419
|
+
}
|
|
420
|
+
|
|
292
421
|
// ----- renovación AUTOMÁTICA del cert (sin QR ni aprobación) -----
|
|
293
422
|
// Con el cert aún vigente y quedando <15 días, cualquier uso del vault dispara en
|
|
294
423
|
// segundo plano un `vault.renew`: el vault firma un cert fresco (30 días) para la
|
|
@@ -305,7 +434,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
305
434
|
if (v.cert.exp <= now || v.cert.exp - now > RENEW_WINDOW_MS) return
|
|
306
435
|
if (now - renewLastTry < RENEW_RETRY_MS) return
|
|
307
436
|
renewLastTry = now
|
|
308
|
-
remoteRenew({ master: v.master, proxy: v.proxy, device, cert: v.cert }).then(({ cert }) => {
|
|
437
|
+
remoteRenew({ master: v.master, proxy: v.proxy, device, cert: v.cert, onRevoked: wipeVaultLink }).then(({ cert }) => {
|
|
309
438
|
kv.setItem(VAULT_CERT_STORAGE, JSON.stringify({ ...v, cert, renewedAt: Date.now() }))
|
|
310
439
|
emitVault({ phase: 'renewed', exp: cert.exp })
|
|
311
440
|
}).catch(() => {}) // best-effort: el cert vigente sigue sirviendo mientras tanto
|
|
@@ -548,7 +677,8 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
548
677
|
// nada que lea datos o firme).
|
|
549
678
|
const LOCK_EXEMPT = new Set([
|
|
550
679
|
'profileLockStatus', 'unlockProfile', 'listProfiles', 'currentProfile',
|
|
551
|
-
'switchProfile', 'createProfile'
|
|
680
|
+
'switchProfile', 'createProfile',
|
|
681
|
+
'profileActa', 'profileMembers', 'myMembership', 'isMaster'
|
|
552
682
|
])
|
|
553
683
|
|
|
554
684
|
const handlers = {
|
|
@@ -735,11 +865,33 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
735
865
|
return rec
|
|
736
866
|
},
|
|
737
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Firma. Con `sign` en el acta, firma aquí mismo (como siempre). Si este dispositivo
|
|
870
|
+
* RENUNCIÓ a firmar —o el master se lo quitó— la petición se re-enruta a quien sí
|
|
871
|
+
* firma (tu bóveda) y vuelve su firma: la identidad de cara a los demás sigue siendo
|
|
872
|
+
* UNA, y este aparato deja de poder firmar por ti aunque lo roben.
|
|
873
|
+
*
|
|
874
|
+
* El `identify` del transporte es la excepción y SIEMPRE se firma en local: es lo que
|
|
875
|
+
* identifica esta conexión ante el proxy, no una firma tuya de cara a nadie, y sin él
|
|
876
|
+
* el dispositivo no podría ni hablar con la bóveda para pedirle que firme.
|
|
877
|
+
*/
|
|
738
878
|
async signData ({ data }) {
|
|
739
879
|
if (data == null) throw new Error('data required')
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
880
|
+
const local = async () => {
|
|
881
|
+
const bytes = new TextEncoder().encode(canonicalStringify(data))
|
|
882
|
+
return { signature: await signBytes(keypair.privateKey, bytes), publickey: publickeyJwkStr }
|
|
883
|
+
}
|
|
884
|
+
const acta = loadActa()
|
|
885
|
+
const puedeFirmar = !acta || Acta.memberCan(acta, publickeyJwkStr, 'sign', loadRenounces())
|
|
886
|
+
if (puedeFirmar || data?.op === 'identify') return local()
|
|
887
|
+
|
|
888
|
+
const v = loadVaultCert(); const device = loadVaultDevice()
|
|
889
|
+
if (!v?.cert || !device) {
|
|
890
|
+
throw new Error('perfil-sin-firmante: este dispositivo ya no firma por ti y no está conectado a ninguna bóveda que pueda hacerlo')
|
|
891
|
+
}
|
|
892
|
+
maybeRenewVaultCert()
|
|
893
|
+
try { return await remoteSign({ master: v.master, proxy: v.proxy, device, cert: v.cert, payload: data, onRevoked: wipeVaultLink }) }
|
|
894
|
+
catch (e) { return handleVaultError(e) }
|
|
743
895
|
},
|
|
744
896
|
|
|
745
897
|
// ----- delegación de capacidad: la maestra firma un cert para una sub-clave -----
|
|
@@ -799,6 +951,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
799
951
|
me = { publickey: publickeyJwkStr, encryptionPubkey: encPublickeyJwkStr, nickname: String(name || '').slice(0, 40) }
|
|
800
952
|
saveMe(me)
|
|
801
953
|
const list = loadProfiles(); list.push({ id: pid, name: me.nickname, pubkey: publickeyJwkStr }); saveProfiles(list)
|
|
954
|
+
await ensureActa(me.nickname) // el perfil nuevo nace con su acta (él mismo es el master)
|
|
802
955
|
return { id: pid, name: me.nickname, pubkey: publickeyJwkStr }
|
|
803
956
|
},
|
|
804
957
|
async switchProfile ({ id } = {}) {
|
|
@@ -819,7 +972,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
819
972
|
if (!list.find((p) => p.id === id)) throw new Error('perfil no existe')
|
|
820
973
|
list = list.filter((p) => p.id !== id); saveProfiles(list)
|
|
821
974
|
// Borrado directo del namespace del perfil (incluye su store del vault si lo tuviera).
|
|
822
|
-
for (const s of ['keypair', 'enc-keypair', 'me', 'nonces', 'delegations', 'revocations', 'vault.device', 'vault.cert']) {
|
|
975
|
+
for (const s of ['keypair', 'enc-keypair', 'me', 'nonces', 'delegations', 'revocations', 'vault.device', 'vault.cert', 'acta', 'renounced']) {
|
|
823
976
|
rawKv.removeItem(`dotrino.identity.p.${id}.${s}`)
|
|
824
977
|
}
|
|
825
978
|
// …y sus CryptoKeys no extractables del keyStore (IndexedDB).
|
|
@@ -832,6 +985,108 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
832
985
|
return { ok: true, current: currentPid }
|
|
833
986
|
},
|
|
834
987
|
|
|
988
|
+
// ----- ACTA DE PERFIL -----
|
|
989
|
+
// Quién es de este perfil y qué puede hacer cada uno. Solo el master sella; los demás
|
|
990
|
+
// adoptan. Ver `dotrino-vault/docs/acta-de-perfil.md`.
|
|
991
|
+
|
|
992
|
+
async profileActa () {
|
|
993
|
+
const acta = loadActa()
|
|
994
|
+
if (!acta) return null
|
|
995
|
+
return { acta, isMaster: amMaster(), myCaps: Acta.effectiveCaps(acta, publickeyJwkStr, loadRenounces()) }
|
|
996
|
+
},
|
|
997
|
+
|
|
998
|
+
async profileMembers () {
|
|
999
|
+
const acta = loadActa()
|
|
1000
|
+
if (!acta) return { members: [], profileId: null, seq: 0, sealer: null }
|
|
1001
|
+
const pend = loadRenounces()
|
|
1002
|
+
const members = await Promise.all(acta.members.map(async (m) => ({
|
|
1003
|
+
pub: m.pub,
|
|
1004
|
+
id: await Acta.memberId(m.pub),
|
|
1005
|
+
label: m.label || '',
|
|
1006
|
+
caps: Acta.effectiveCaps(acta, m.pub, pend),
|
|
1007
|
+
addedAt: m.addedAt || null,
|
|
1008
|
+
isMe: m.pub === publickeyJwkStr,
|
|
1009
|
+
isMaster: m.pub === acta.sealer
|
|
1010
|
+
})))
|
|
1011
|
+
return { members, profileId: acta.profileId, seq: acta.seq, sealer: acta.sealer, updatedAt: acta.updatedAt }
|
|
1012
|
+
},
|
|
1013
|
+
|
|
1014
|
+
async myMembership () {
|
|
1015
|
+
const acta = loadActa()
|
|
1016
|
+
if (!acta) return { inProfile: false }
|
|
1017
|
+
const m = acta.members.find((x) => x.pub === publickeyJwkStr)
|
|
1018
|
+
return {
|
|
1019
|
+
inProfile: !!m,
|
|
1020
|
+
profileId: acta.profileId,
|
|
1021
|
+
seq: acta.seq,
|
|
1022
|
+
isMaster: acta.sealer === publickeyJwkStr,
|
|
1023
|
+
caps: Acta.effectiveCaps(acta, publickeyJwkStr, loadRenounces()),
|
|
1024
|
+
id: m ? await Acta.memberId(m.pub) : null
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
|
|
1028
|
+
async isMaster () { return amMaster() },
|
|
1029
|
+
|
|
1030
|
+
/** Admite un miembro (solo el master). El cert lo emite quien llama, antes o después. */
|
|
1031
|
+
async admitMember ({ pub, encPub = null, label = '', caps = ['store', 'read'], cert = null } = {}) {
|
|
1032
|
+
const acta = await sealChanges([{ op: 'admit', member: { pub, encPub, label, caps, cert } }])
|
|
1033
|
+
return { ok: true, seq: acta.seq }
|
|
1034
|
+
},
|
|
1035
|
+
|
|
1036
|
+
async setCaps ({ pub, caps } = {}) {
|
|
1037
|
+
const acta = await sealChanges([{ op: 'caps', pub, caps }])
|
|
1038
|
+
return { ok: true, seq: acta.seq }
|
|
1039
|
+
},
|
|
1040
|
+
|
|
1041
|
+
async removeMember ({ pub } = {}) {
|
|
1042
|
+
const acta = await sealChanges([{ op: 'remove', pub }])
|
|
1043
|
+
return { ok: true, seq: acta.seq }
|
|
1044
|
+
},
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Traspasa el master a otro miembro. Admitir y nombrar van en el MISMO seq: el nuevo
|
|
1048
|
+
* sellador tiene que ser miembro para poder serlo, y así no hay ventana intermedia.
|
|
1049
|
+
* Cubre igual dispositivo → bóveda y bóveda → bóveda (mudarse de PC).
|
|
1050
|
+
*/
|
|
1051
|
+
async handoverMaster ({ to, member = null } = {}) {
|
|
1052
|
+
const changes = []
|
|
1053
|
+
if (member) changes.push({ op: 'admit', member: { ...member, pub: to } })
|
|
1054
|
+
changes.push({ op: 'handover', to })
|
|
1055
|
+
const acta = await sealChanges(changes)
|
|
1056
|
+
return { ok: true, seq: acta.seq, sealer: acta.sealer }
|
|
1057
|
+
},
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* RENUNCIA (§2.2): este dispositivo se quita capacidades a sí mismo. No pasa por el
|
|
1061
|
+
* master —por eso funciona con la bóveda apagada, que es justo cuando hace falta (te
|
|
1062
|
+
* robaron el aparato)— y solo puede QUITAR, así que cualquiera puede honrarla.
|
|
1063
|
+
*/
|
|
1064
|
+
async renounceCaps ({ caps } = {}) {
|
|
1065
|
+
const acta = loadActa()
|
|
1066
|
+
if (!acta) throw new Error('este perfil todavía no tiene acta')
|
|
1067
|
+
const record = await Acta.makeRenounce({ member: publickeyJwkStr, caps, privateKey: keypair.privateKey })
|
|
1068
|
+
const pend = loadRenounces().filter((r) => r.member !== publickeyJwkStr)
|
|
1069
|
+
pend.push(record)
|
|
1070
|
+
saveRenounces(pend)
|
|
1071
|
+
emitVault({ phase: 'renounced', caps: record.caps })
|
|
1072
|
+
// Si además soy el master, la absorbo ya en el acta.
|
|
1073
|
+
if (amMaster()) { try { await sealChanges([{ op: 'renounce', record }]) } catch (_) {} }
|
|
1074
|
+
return { ok: true, record, caps: Acta.effectiveCaps(loadActa(), publickeyJwkStr, loadRenounces()) }
|
|
1075
|
+
},
|
|
1076
|
+
|
|
1077
|
+
/** Absorbe en el acta una renuncia ajena ya verificada (solo el master). */
|
|
1078
|
+
async absorbRenounce ({ record } = {}) {
|
|
1079
|
+
if (!(await Acta.verifyRenounce(record))) throw new Error('renuncia inválida: la firma no es del propio miembro')
|
|
1080
|
+
const acta = await sealChanges([{ op: 'renounce', record }])
|
|
1081
|
+
return { ok: true, seq: acta.seq }
|
|
1082
|
+
},
|
|
1083
|
+
|
|
1084
|
+
/** Adopta un acta que llega de otro miembro (gana el seq mayor; a igual seq, el traspaso). */
|
|
1085
|
+
async adoptActa ({ acta } = {}) { return adoptActa(acta) },
|
|
1086
|
+
|
|
1087
|
+
/** Une este dispositivo al perfil de otra bóveda (solo si aquí no hay nada que perder). */
|
|
1088
|
+
async joinProfile ({ acta } = {}) { return joinProfile(acta) },
|
|
1089
|
+
|
|
835
1090
|
// ----- emparejar ESTE dispositivo con el vault del usuario (Fase 1) -----
|
|
836
1091
|
// Genera D aquí dentro (su privada NUNCA sale de la identidad), hace el enroll
|
|
837
1092
|
// endurecido por el proxy y guarda el cert. NO cambia signData todavía (Fase 2).
|
|
@@ -844,9 +1099,11 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
844
1099
|
const res = await remoteEnroll({ qr, device, onChallenge: (c) => emitVault({ phase: 'challenge', deviceId: c.deviceId, code: c.code }) })
|
|
845
1100
|
kv.setItem(VAULT_DEVICE_STORAGE, JSON.stringify({ useIdentityKey: true, publickey: publickeyJwkStr }))
|
|
846
1101
|
kv.setItem(VAULT_CERT_STORAGE, JSON.stringify({ cert: res.cert, master: res.master, proxy: res.proxy, deviceId: res.deviceId, pairedAt: Date.now() }))
|
|
847
|
-
|
|
1102
|
+
// Conectarse a una bóveda es ENTRAR A SU PERFIL: el acta viene con el cert.
|
|
1103
|
+
const join = res.acta ? await joinProfile(res.acta) : { joined: false, reason: 'sin-acta' }
|
|
1104
|
+
emitVault({ phase: 'paired', deviceId: res.deviceId, master: res.master, join })
|
|
848
1105
|
pullProfileFromVault() // adoptar el perfil que ya viva en el vault (si hay)
|
|
849
|
-
return { ok: true, deviceId: res.deviceId, master: res.master, exp: res.cert.exp, scope: res.cert.scope }
|
|
1106
|
+
return { ok: true, deviceId: res.deviceId, master: res.master, exp: res.cert.exp, scope: res.cert.scope, join }
|
|
850
1107
|
},
|
|
851
1108
|
|
|
852
1109
|
async vaultStatus () {
|
|
@@ -870,7 +1127,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
870
1127
|
const v = loadVaultCert(); const device = loadVaultDevice()
|
|
871
1128
|
if (!v?.cert || !device) throw new Error('este dispositivo no está emparejado con un vault')
|
|
872
1129
|
maybeRenewVaultCert()
|
|
873
|
-
try { return await remoteSign({ master: v.master, proxy: v.proxy, device, cert: v.cert, payload }) }
|
|
1130
|
+
try { return await remoteSign({ master: v.master, proxy: v.proxy, device, cert: v.cert, payload, onRevoked: wipeVaultLink }) }
|
|
874
1131
|
catch (e) { return handleVaultError(e) }
|
|
875
1132
|
},
|
|
876
1133
|
|
|
@@ -880,7 +1137,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
880
1137
|
const v = loadVaultCert(); const device = loadVaultDevice()
|
|
881
1138
|
if (!v?.cert || !device) throw new Error('este dispositivo no está emparejado con un vault')
|
|
882
1139
|
maybeRenewVaultCert()
|
|
883
|
-
try { return await remoteStore({ master: v.master, proxy: v.proxy, device, cert: v.cert, method, args }) }
|
|
1140
|
+
try { return await remoteStore({ master: v.master, proxy: v.proxy, device, cert: v.cert, method, args, onRevoked: wipeVaultLink }) }
|
|
884
1141
|
catch (e) { return handleVaultError(e) }
|
|
885
1142
|
},
|
|
886
1143
|
|
|
@@ -889,8 +1146,12 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
889
1146
|
const v = loadVaultCert(); const device = loadVaultDevice()
|
|
890
1147
|
if (!v?.cert || !device) throw new Error('este dispositivo no está emparejado con un vault')
|
|
891
1148
|
maybeRenewVaultCert()
|
|
892
|
-
try {
|
|
893
|
-
|
|
1149
|
+
try {
|
|
1150
|
+
const res = await remoteDevices({ master: v.master, proxy: v.proxy, device, cert: v.cert, onRevoked: wipeVaultLink })
|
|
1151
|
+
// El acta viaja con la lista: así los cambios de política llegan sin canal aparte.
|
|
1152
|
+
if (res.acta) { try { await (res.acta.profileId === loadActa()?.profileId ? adoptActa(res.acta) : joinProfile(res.acta)) } catch (_) {} }
|
|
1153
|
+
return res
|
|
1154
|
+
} catch (e) { return handleVaultError(e) }
|
|
894
1155
|
},
|
|
895
1156
|
|
|
896
1157
|
// El cert de delegación de este dispositivo (para presentarlo al proxy en `identify`
|
|
@@ -1082,6 +1343,9 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1082
1343
|
me = { publickey: publickeyJwkStr, encryptionPubkey: encPublickeyJwkStr }
|
|
1083
1344
|
kv.setItem(ME_STORAGE, JSON.stringify(me))
|
|
1084
1345
|
}
|
|
1346
|
+
// Acta de perfil: si no existe, nace ahora (un miembro, este dispositivo, que es el master).
|
|
1347
|
+
try { await ensureActa() } catch (e) { console.warn('[identity] no se pudo crear el acta de perfil:', e.message) }
|
|
1348
|
+
|
|
1085
1349
|
// Perfil compartido: jalar del vault en background (gana el más nuevo).
|
|
1086
1350
|
pullProfileFromVault()
|
|
1087
1351
|
|
package/vault/remote.js
CHANGED
|
@@ -5,22 +5,49 @@
|
|
|
5
5
|
* —cuya privada NUNCA sale de la identidad—, hace el emparejamiento ENDURECIDO por el
|
|
6
6
|
* proxy (ver dotrino-vault/docs/pairing-protocol.md) y devuelve el cert ya validado.
|
|
7
7
|
*
|
|
8
|
-
* Flujo:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Flujo: genera un código de 6 dígitos, lo MUESTRA y manda solo su COMPROMISO dentro del
|
|
9
|
+
* ENROLL firmado con D (prueba de posesión) → el dueño tipea el código en la bóveda, que
|
|
10
|
+
* lo comprueba contra el compromiso y solo entonces firma → el dispositivo acepta el cert
|
|
11
|
+
* si le ECHAN su código y lo valida (firmado por la maestra que vio en el QR, y para SU clave).
|
|
11
12
|
*
|
|
12
13
|
* No reimplementa cripto: usa `@dotrino/identity/capabilities`. Transporte:
|
|
13
14
|
* `@dotrino/proxy-client` (importado perezosamente; solo se carga al emparejar).
|
|
14
15
|
*/
|
|
15
|
-
import { makeDeviceKey, signWithDevice, verifyDelegation, makePairingCode, pubkeyId } from './capabilities.js'
|
|
16
|
+
import { makeDeviceKey, signWithDevice, verifyDelegation, verifyDeviceSig, makePairingCode, commitCode, pubkeyId } from './capabilities.js'
|
|
16
17
|
|
|
17
18
|
const MSG = {
|
|
18
19
|
ENROLL: 'vault.enroll',
|
|
19
20
|
ENROLL_CHALLENGE: 'vault.enroll.challenge',
|
|
20
21
|
ENROLLED: 'vault.enrolled',
|
|
22
|
+
REVOKED: 'vault.revoked',
|
|
21
23
|
ERROR: 'vault.error'
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
/**
|
|
27
|
+
* ¿Es AUTÉNTICO este `vault.revoked`? Solo lo es si va firmado por la maestra PINEADA al
|
|
28
|
+
* emparejar, es para ESTE dispositivo y no ha caducado. Es la única puerta al autoborrado:
|
|
29
|
+
* un `vault.error` con la palabra «revocado» no borra nada (cierra el wipe-DoS, ver
|
|
30
|
+
* `dotrino-vault/docs/pairing-protocol.md §2.3`).
|
|
31
|
+
*/
|
|
32
|
+
export async function isAuthenticRevoke ({ body, signature, master, devicePubkey }) {
|
|
33
|
+
if (!body || body.op !== 'revoke' || typeof signature !== 'string') return false
|
|
34
|
+
if (body.sub !== devicePubkey) return false
|
|
35
|
+
if (typeof body.exp === 'number' && Date.now() > body.exp) return false
|
|
36
|
+
return verifyDeviceSig({ publickey: master, data: body, signature })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Identifica la conexión bajo la pubkey de este dispositivo. Además de hacerlo
|
|
41
|
+
* direccionable, es lo que hace que el proxy le entregue lo que tenía ENCOLADO (24 h) —
|
|
42
|
+
* entre otras cosas, un `vault.revoked` emitido mientras estaba apagado.
|
|
43
|
+
*/
|
|
44
|
+
async function identifyAsDevice (client, device) {
|
|
45
|
+
if (!client.token) return
|
|
46
|
+
const data = { op: 'identify', publickey: device.publickey, token: client.token, ts: Date.now() }
|
|
47
|
+
const { signature } = await signWithDevice({ privateJwk: device.privateJwk, privateKey: device.privateKey, publickey: device.publickey, data })
|
|
48
|
+
await client.identify({ data, signature })
|
|
49
|
+
}
|
|
50
|
+
|
|
24
51
|
/**
|
|
25
52
|
* @param {Object} opts
|
|
26
53
|
* @param {{v:number, iss:string, proxy:string, token:string, sn:string}} opts.qr QR v2 del vault.
|
|
@@ -43,9 +70,12 @@ export async function enrollDevice ({ qr, device, onChallenge, label = '', appro
|
|
|
43
70
|
// El DISPOSITIVO genera el código y manda solo su COMPROMISO (no el código). El vault
|
|
44
71
|
// aprende el código únicamente cuando vos lo tipeás en el PC → aprobar exige tener el dispositivo.
|
|
45
72
|
const code = makePairingCode()
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
73
|
+
// Se manda el COMPROMISO del código, nunca el código. El vault lo aprende solo cuando lo
|
|
74
|
+
// tipeas, recompone el compromiso y únicamente entonces firma el cert → aprobar exige
|
|
75
|
+
// haber leído el código de ESTA pantalla. Y al ECHARLO de vuelta, el dispositivo confía:
|
|
76
|
+
// una bóveda falsa no conoce el código y no puede enrolarlo.
|
|
77
|
+
const commit = await commitCode({ code, dpub: dev.publickey, sn: qr.sn })
|
|
78
|
+
const data = { op: 'enroll', dpub: dev.publickey, token: qr.token, sn: qr.sn, commit, label, ts: Date.now() }
|
|
49
79
|
const { signature } = await signWithDevice({ privateJwk: dev.privateJwk, privateKey: dev.privateKey, publickey: dev.publickey, data })
|
|
50
80
|
|
|
51
81
|
const enrolled = new Promise((resolve, reject) => {
|
|
@@ -68,7 +98,7 @@ export async function enrollDevice ({ qr, device, onChallenge, label = '', appro
|
|
|
68
98
|
if (!v.ok) throw new Error('cert inválido: ' + v.reason)
|
|
69
99
|
if (res.cert.iss !== qr.iss) throw new Error('cert firmado por una maestra distinta a la que viste')
|
|
70
100
|
if (res.cert.sub !== dev.publickey) throw new Error('cert emitido para otro dispositivo')
|
|
71
|
-
return { device: dev, cert: res.cert, master: qr.iss, proxy: qr.proxy, deviceId }
|
|
101
|
+
return { device: dev, cert: res.cert, master: qr.iss, proxy: qr.proxy, deviceId, acta: res.acta || null }
|
|
72
102
|
} finally { try { client.close() } catch (_) {} }
|
|
73
103
|
}
|
|
74
104
|
|
|
@@ -78,41 +108,39 @@ export async function enrollDevice ({ qr, device, onChallenge, label = '', appro
|
|
|
78
108
|
* firma. Requiere que el vault esté online.
|
|
79
109
|
* @returns {Promise<{ signature:string, publickey:string }>} publickey = la maestra.
|
|
80
110
|
*/
|
|
81
|
-
export async function requestSign ({ master, proxy, device, cert, payload, timeoutMs = 15000 } = {}) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const data = { op: 'sign', payload, publickey: device.publickey, ts: Date.now() }
|
|
88
|
-
const { signature } = await signWithDevice({ privateJwk: device.privateJwk, privateKey: device.privateKey, publickey: device.publickey, data })
|
|
89
|
-
const pending = new Promise((resolve, reject) => {
|
|
90
|
-
const off = client.on('message', (_f, p) => {
|
|
91
|
-
if (!p || typeof p !== 'object') return
|
|
92
|
-
if (p.type === 'vault.signed') { cleanup(); resolve(p) }
|
|
93
|
-
else if (p.type === 'vault.error') { cleanup(); reject(new Error(p.error)) }
|
|
94
|
-
})
|
|
95
|
-
const t = setTimeout(() => { cleanup(); reject(new Error('el vault no respondió (¿está encendido?)')) }, timeoutMs)
|
|
96
|
-
const cleanup = () => { off(); clearTimeout(t) }
|
|
97
|
-
})
|
|
98
|
-
client.sendByPubkey(master, { type: 'vault.sign', data, signature, cert })
|
|
99
|
-
const res = await pending
|
|
100
|
-
return { signature: res.signature, publickey: res.publickey }
|
|
101
|
-
} finally { try { client.close() } catch (_) {} }
|
|
111
|
+
export async function requestSign ({ master, proxy, device, cert, payload, onRevoked, timeoutMs = 15000 } = {}) {
|
|
112
|
+
const res = await vaultRpc({
|
|
113
|
+
master, proxy, device, cert, onRevoked, timeoutMs,
|
|
114
|
+
sendType: 'vault.sign', okType: 'vault.signed', data: { op: 'sign', payload }
|
|
115
|
+
})
|
|
116
|
+
return { signature: res.signature, publickey: res.publickey }
|
|
102
117
|
}
|
|
103
118
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Helper genérico: una RPC al vault firmada por D + cert, esperando `okType`.
|
|
121
|
+
*
|
|
122
|
+
* Se identifica al conectar para que el proxy entregue lo ENCOLADO: si mientras el
|
|
123
|
+
* dispositivo estaba apagado la bóveda emitió un `vault.revoked` firmado, llega aquí y se
|
|
124
|
+
* ejecuta el autoborrado (`onRevoked`) tras verificar la firma contra la maestra pineada.
|
|
125
|
+
*/
|
|
126
|
+
async function vaultRpc ({ master, proxy, device, cert, sendType, okType, data, onRevoked, timeoutMs = 15000 }) {
|
|
106
127
|
if (!master || !proxy || !(device?.privateJwk || device?.privateKey) || !cert) throw new Error('faltan datos de emparejamiento')
|
|
107
128
|
const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
|
|
108
129
|
const client = new WebSocketProxyClient({ url: proxy, enableWebRTC: false, autoReconnect: false })
|
|
109
130
|
await client.connect()
|
|
110
131
|
try {
|
|
132
|
+
try { await identifyAsDevice(client, device) } catch (_) { /* sin identify seguimos: solo perdemos la cola */ }
|
|
111
133
|
const signed = { ...data, publickey: device.publickey, ts: Date.now() }
|
|
112
134
|
const { signature } = await signWithDevice({ privateJwk: device.privateJwk, privateKey: device.privateKey, publickey: device.publickey, data: signed })
|
|
113
135
|
const pending = new Promise((resolve, reject) => {
|
|
114
136
|
const off = client.on('message', (_f, p) => {
|
|
115
137
|
if (!p || typeof p !== 'object') return
|
|
138
|
+
if (p.type === MSG.REVOKED) {
|
|
139
|
+
isAuthenticRevoke({ body: p.body, signature: p.signature, master, devicePubkey: device.publickey })
|
|
140
|
+
.then((ok) => { if (ok) { try { onRevoked?.() } catch (_) {} } })
|
|
141
|
+
.catch(() => {})
|
|
142
|
+
return
|
|
143
|
+
}
|
|
116
144
|
if (p.type === okType) { cleanup(); resolve(p) }
|
|
117
145
|
else if (p.type === 'vault.error') { cleanup(); reject(new Error(p.error)) }
|
|
118
146
|
})
|
|
@@ -125,15 +153,15 @@ async function vaultRpc ({ master, proxy, device, cert, sendType, okType, data,
|
|
|
125
153
|
}
|
|
126
154
|
|
|
127
155
|
/** Lee/escribe el store de hilos+aperturas EN el vault (con el cert del dispositivo). */
|
|
128
|
-
export async function requestStore ({ master, proxy, device, cert, method, args } = {}) {
|
|
129
|
-
const res = await vaultRpc({ master, proxy, device, cert, sendType: 'vault.store', okType: 'vault.store.result', data: { op: 'store', method, args: args || {} } })
|
|
156
|
+
export async function requestStore ({ master, proxy, device, cert, method, args, onRevoked } = {}) {
|
|
157
|
+
const res = await vaultRpc({ master, proxy, device, cert, onRevoked, sendType: 'vault.store', okType: 'vault.store.result', data: { op: 'store', method, args: args || {} } })
|
|
130
158
|
return res.result
|
|
131
159
|
}
|
|
132
160
|
|
|
133
161
|
/** Lista (solo lectura) los dispositivos enrolados en tu vault. */
|
|
134
|
-
export async function requestDevices ({ master, proxy, device, cert } = {}) {
|
|
135
|
-
const res = await vaultRpc({ master, proxy, device, cert, sendType: 'vault.devices', okType: 'vault.devices.result', data: { op: 'devices' } })
|
|
136
|
-
return { devices: res.devices || [], revoked: res.revoked || [] }
|
|
162
|
+
export async function requestDevices ({ master, proxy, device, cert, onRevoked } = {}) {
|
|
163
|
+
const res = await vaultRpc({ master, proxy, device, cert, onRevoked, sendType: 'vault.devices', okType: 'vault.devices.result', data: { op: 'devices' } })
|
|
164
|
+
return { devices: res.devices || [], revoked: res.revoked || [], acta: res.acta || null }
|
|
137
165
|
}
|
|
138
166
|
|
|
139
167
|
/**
|
|
@@ -141,8 +169,8 @@ export async function requestDevices ({ master, proxy, device, cert } = {}) {
|
|
|
141
169
|
* el vault firma uno fresco para la misma sub-clave y scope, sin QR ni aprobación.
|
|
142
170
|
* @returns {Promise<{ cert: object }>}
|
|
143
171
|
*/
|
|
144
|
-
export async function requestRenew ({ master, proxy, device, cert } = {}) {
|
|
145
|
-
const res = await vaultRpc({ master, proxy, device, cert, sendType: 'vault.renew', okType: 'vault.renewed', data: { op: 'renew' } })
|
|
172
|
+
export async function requestRenew ({ master, proxy, device, cert, onRevoked } = {}) {
|
|
173
|
+
const res = await vaultRpc({ master, proxy, device, cert, onRevoked, sendType: 'vault.renew', okType: 'vault.renewed', data: { op: 'renew' } })
|
|
146
174
|
if (!res.cert || res.cert.sub !== device.publickey || res.cert.iss !== master) throw new Error('cert renovado inválido')
|
|
147
175
|
return { cert: res.cert }
|
|
148
176
|
}
|
package/vault/vault.js
CHANGED
|
@@ -110,12 +110,16 @@ import { pubkeyId } from './capabilities.js'
|
|
|
110
110
|
|
|
111
111
|
// Adaptador: startDeviceVault exige identity.{me.publickey, signData, signDelegation,
|
|
112
112
|
// listDelegations, revokeDelegation}; el core los expone vía handlers + getter me.
|
|
113
|
+
// `admitMember`/`profileActa` son opcionales para el mostrador de enrolamiento, pero sin
|
|
114
|
+
// ellos aprobar emitiría el cert SIN meter al dispositivo en el acta: se pasan también.
|
|
113
115
|
const selfIdentity = {
|
|
114
116
|
get me () { return core.me },
|
|
115
117
|
signData: (data) => handlers.signData({ data }),
|
|
116
118
|
signDelegation: (sub, scope, opts) => handlers.signDelegation({ sub, scope, ...(opts || {}) }),
|
|
117
119
|
listDelegations: () => handlers.listDelegations({}),
|
|
118
|
-
revokeDelegation: (nonce) => handlers.revokeDelegation({ nonce })
|
|
120
|
+
revokeDelegation: (nonce) => handlers.revokeDelegation({ nonce }),
|
|
121
|
+
admitMember: (m) => handlers.admitMember(m),
|
|
122
|
+
profileActa: () => handlers.profileActa({})
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
async function startSelfDaemon () {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/vault@0.
|
|
2
|
-
El iframe de identity se sirve
|
|
3
|
-
resuelve en el navegador sin bundler.
|
|
4
|
-
|
|
5
|
-
el import map de index.html.
|
|
1
|
+
Copia vendorizada de @dotrino/vault@0.5.0 (lib/src/{index,enroll}.js, sin dependencias).
|
|
2
|
+
El iframe de identity se sirve estatico (vanilla, sin build); asi startDeviceVault
|
|
3
|
+
resuelve en el navegador sin bundler. index.js importa ./enroll.js (relativo, se
|
|
4
|
+
vendoriza tambien) y @dotrino/identity/capabilities (=../../capabilities.js) y
|
|
5
|
+
@dotrino/proxy-client (=../proxy-client/), ambos via el import map de index.html.
|
|
6
|
+
Re-vendorizar AMBOS archivos al subir @dotrino/vault.
|