@dotrino/identity 0.52.0 → 0.54.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/identity",
3
- "version": "0.52.0",
3
+ "version": "0.54.0",
4
4
  "description": "Identidad y rating de usuarios compartidos entre apps de Dotrino (vault iframe + postMessage)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -56,11 +56,11 @@
56
56
  "url": "git+https://github.com/imdotrino/dotrino-identity.git"
57
57
  },
58
58
  "dependencies": {
59
- "@dotrino/proxy-client": "0.10.0"
59
+ "@dotrino/proxy-client": "0.10.1"
60
60
  },
61
61
  "devDependencies": {
62
62
  "fake-indexeddb": "^6.2.5",
63
- "typescript": "^5.7.3",
63
+ "typescript": "5.9.3",
64
64
  "@types/node": "^22.0.0"
65
65
  }
66
66
  }
package/src/index.js CHANGED
@@ -292,6 +292,8 @@ export class Identity {
292
292
 
293
293
  /** Admite un miembro nuevo (solo el master). */
294
294
  async admitMember (member) { return this._call('admitMember', member) }
295
+ /** Registra la llave de cifrado de un miembro ya admitido (evita re-enrolarlo). */
296
+ async setMemberEncPub (args) { return this._call('setMemberEncPub', args) }
295
297
 
296
298
  /** Cambia las capacidades de un miembro (solo el master). */
297
299
  async setCaps (pub, caps) { return this._call('setCaps', { pub, caps }) }
@@ -671,4 +673,4 @@ export class Identity {
671
673
 
672
674
  // Helpers de capacidad SIN clave maestra (lado dispositivo + verificación), reutilizables
673
675
  // por apps/bridges sin cargar el iframe del vault.
674
- export { makeDeviceKey, signWithDevice, verifyDelegation, verifyChain, pubkeyId, deriveSAS, verifyDeviceSig, makePairingCode, commitCode, avatarSvg, avatarDataUri, MAX_DELEGATION_MS, DEFAULT_DELEGATION_MS } from '../vault/capabilities.js'
676
+ export { makeDeviceKey, makeDeviceEncKey, importDeviceEncKey, signWithDevice, verifyDelegation, verifyChain, pubkeyId, deriveSAS, verifyDeviceSig, makePairingCode, commitCode, avatarSvg, avatarDataUri, MAX_DELEGATION_MS, DEFAULT_DELEGATION_MS } from '../vault/capabilities.js'
package/src/node.js CHANGED
@@ -184,6 +184,8 @@ export class Identity {
184
184
  actaHistory (opts) { return this._h('actaHistory', opts || {}) }
185
185
  isMaster () { return this._h('isMaster') }
186
186
  admitMember (member) { return this._h('admitMember', member) }
187
+ /** Registra la llave de cifrado de un miembro ya admitido (evita re-enrolarlo). */
188
+ setMemberEncPub (args) { return this._h('setMemberEncPub', args) }
187
189
  setCaps (pub, caps) { return this._h('setCaps', { pub, caps }) }
188
190
  setLabel (pub, label) { return this._h('setLabel', { pub, label }) }
189
191
  removeMember (pub) { return this._h('removeMember', { pub }) }
@@ -269,4 +271,4 @@ export default Identity
269
271
 
270
272
  // Helpers de capacidad SIN clave maestra (lado dispositivo + verificación), para que
271
273
  // un bridge/bot Node pueda crear su clave, firmar acciones y verificar cadenas D←P.
272
- export { makeDeviceKey, signWithDevice, verifyDelegation, verifyChain, pubkeyId, deriveSAS, verifyDeviceSig, makePairingCode, commitCode, avatarSvg, avatarDataUri, MAX_DELEGATION_MS, DEFAULT_DELEGATION_MS } from '../vault/capabilities.js'
274
+ export { makeDeviceKey, makeDeviceEncKey, importDeviceEncKey, signWithDevice, verifyDelegation, verifyChain, pubkeyId, deriveSAS, verifyDeviceSig, makePairingCode, commitCode, avatarSvg, avatarDataUri, MAX_DELEGATION_MS, DEFAULT_DELEGATION_MS } from '../vault/capabilities.js'
package/vault/acta.js CHANGED
@@ -30,7 +30,16 @@
30
30
  import { canonicalStringify } from './core.js'
31
31
  import { signWithDevice, verifyDeviceSig, pubkeyId } from './capabilities.js'
32
32
 
33
- export const ACTA_V = 1
33
+ export const ACTA_V = 2
34
+
35
+ /**
36
+ * Versiones de acta que se ACEPTAN al leer. La 1 sigue entrando porque una v1 en el disco
37
+ * es un perfil con sus aparatos dentro: rechazarla dejaría al vault sin poder verificar a
38
+ * nadie —y a los servicios sin configuración— por un campo que ni siquiera existía. Se
39
+ * asciende sola: el acta siguiente que selle la maestra ya sale v2 (ver `applyChanges`).
40
+ * La única diferencia es la llave de sellado (§8.9), que en una v1 simplemente no hay.
41
+ */
42
+ const ACTA_LEIBLES = Object.freeze([1, 2])
34
43
 
35
44
  /**
36
45
  * Lista CERRADA de capacidades. Sellar y admitir no están: eso es ser el master.
@@ -82,6 +91,20 @@ export const CAP_SCOPE = Object.freeze({ sign: 'vault:sign', store: 'vault:store
82
91
  const enc = (s) => new TextEncoder().encode(s)
83
92
  const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, '0')).join('')
84
93
  const isPub = (v) => typeof v === 'string' && v.length > 0
94
+
95
+ /**
96
+ * La llave de CIFRADO de un miembro (`encPub`): JWK público de una ECDH P-256.
97
+ * Se valida de verdad —no basta con «es un string»— porque es lo que decide si a
98
+ * ese aparato se le puede envolver un secreto: una `encPub` mal formada no falla al
99
+ * escribir el acta, falla mucho después al intentar sellarle algo.
100
+ */
101
+ const isEncPub = (v) => {
102
+ if (typeof v !== 'string' || !v) return false
103
+ try {
104
+ const j = JSON.parse(v)
105
+ return j?.kty === 'EC' && j?.crv === 'P-256' && typeof j?.x === 'string' && typeof j?.y === 'string'
106
+ } catch (_) { return false }
107
+ }
85
108
  const cleanCaps = (caps) => [...new Set((Array.isArray(caps) ? caps : []).filter((c) => CAPS.includes(c)))].sort()
86
109
 
87
110
  /**
@@ -112,7 +135,7 @@ export const isHandover = (acta) => !!acta && acta.sealer !== acta.sealedBy
112
135
  * sellador, con todas las capacidades. `profileId` = su pubkey → el nombre del perfil es
113
136
  * estable para siempre y coincide con la identidad que el usuario ya tenía (cero migración).
114
137
  */
115
- export function genesisActa ({ pub, encPub = null, label = '', now = Date.now() }) {
138
+ export function genesisActa ({ pub, encPub = null, sealPub = null, label = '', now = Date.now() }) {
116
139
  if (!isPub(pub)) throw new Error('genesisActa: missing genesis pubkey')
117
140
  return {
118
141
  v: ACTA_V,
@@ -127,6 +150,14 @@ export function genesisActa ({ pub, encPub = null, label = '', now = Date.now()
127
150
  // Llavero del contenido: una entrada por generación, con la clave del perfil ENVUELTA
128
151
  // a cada miembro (ver content.js). Envuelto es público: solo lo abre su destinatario.
129
152
  keyring: [],
153
+ // LLAVE DE SELLADO (§8.9 de dotrino-vault/docs/secretos-sellados.md): con ella la
154
+ // bóveda FIRMA los sobres de los secretos, para que se sepa que salieron de ella.
155
+ // Vive aquí y no en un certificado aparte porque rota con el acta: cada acta nueva
156
+ // puede nombrar una llave nueva, y quien selle el acta es —siempre— la maestra.
157
+ // Nace en null: un perfil no tiene por qué sellar secretos.
158
+ sealPub: sealPub || null,
159
+ sealSince: sealPub ? 1 : 0,
160
+ sealKeys: [],
130
161
  updatedAt: now
131
162
  }
132
163
  }
@@ -134,7 +165,7 @@ export function genesisActa ({ pub, encPub = null, label = '', now = Date.now()
134
165
  /** Comprobaciones de FORMA (sin cripto): que el acta sea un acta. */
135
166
  export function checkShape (acta) {
136
167
  if (!acta || typeof acta !== 'object') return 'no-acta'
137
- if (acta.v !== ACTA_V) return 'version'
168
+ if (!ACTA_LEIBLES.includes(acta.v)) return 'version'
138
169
  if (!isPub(acta.profileId) || !isPub(acta.sealer) || !isPub(acta.sealedBy)) return 'shape'
139
170
  if (!Number.isInteger(acta.seq) || acta.seq < 1) return 'seq'
140
171
  if (acta.seq > 1 && typeof acta.prev !== 'string') return 'prev'
@@ -151,6 +182,29 @@ export function checkShape (acta) {
151
182
  } else if (m.caps.includes('secrets')) {
152
183
  return 'secretos-sin-cn'
153
184
  }
185
+ // La llave de cifrado: por ahora se valida la FORMA y solo si viene. NO se exige
186
+ // todavía, aunque el destino sea exigírsela a quien recibe secretos.
187
+ //
188
+ // El motivo es que `checkShape` corre en CADA `verifyActa`, también sobre actas ya
189
+ // selladas: exigirla hoy invalidaría de golpe las actas en las que un servicio
190
+ // entró sin ella —que son todas las anteriores a esto— y el vault dejaría de
191
+ // arrancar en vez de avisar. Primero los servicios registran su llave (op
192
+ // `encpub`), y cuando no quede ninguno sin ella se aprieta aquí.
193
+ if (m.encPub != null && !isEncPub(m.encPub)) return 'encpub-invalido'
194
+ }
195
+ // LA LLAVE DE SELLADO Y SU REGISTRO. `sealPub` puede no estar (un perfil que no sella
196
+ // secretos), pero si está tiene que decir DESDE QUÉ acta manda: sin `sealSince` no se
197
+ // puede decidir con qué llave verificar un sobre viejo.
198
+ if (acta.v >= 2) {
199
+ if (acta.sealPub != null) {
200
+ if (!isPub(acta.sealPub)) return 'sealpub-invalido'
201
+ if (!Number.isInteger(acta.sealSince) || acta.sealSince < 1 || acta.sealSince > acta.seq) return 'sealsince'
202
+ } else if (acta.sealSince) return 'sealsince'
203
+ if (!Array.isArray(acta.sealKeys)) return 'sealkeys'
204
+ for (const k of acta.sealKeys) {
205
+ if (!isPub(k?.pub)) return 'sealkey-invalida'
206
+ if (!Number.isInteger(k.from) || !Number.isInteger(k.to) || k.from < 1 || k.to < k.from) return 'sealkey-rango'
207
+ }
154
208
  }
155
209
  if (new Set(acta.members.map((m) => m.pub)).size !== acta.members.length) return 'miembro-duplicado'
156
210
  if (!acta.members.some((m) => m.pub === acta.sealer)) return 'sealer-no-es-miembro'
@@ -193,7 +247,7 @@ export async function verifyActa ({ acta, expectedProfileId } = {}) {
193
247
  * Van en ARRAY porque hay combinaciones que deben ser atómicas: admitir al nuevo sellador y
194
248
  * traspasarle el master ocurre en el MISMO `seq` (§2.1.3).
195
249
  */
196
- export async function applyChanges (acta, changes, { by, now = Date.now() } = {}) {
250
+ export async function applyChanges (acta, changes, { by, now = Date.now(), sealPub = null } = {}) {
197
251
  const shape = checkShape(acta)
198
252
  if (shape) throw new Error('invalid record: ' + shape)
199
253
  if (!by) throw new Error('applyChanges: missing `by` (who seals)')
@@ -204,6 +258,11 @@ export async function applyChanges (acta, changes, { by, now = Date.now() } = {}
204
258
 
205
259
  const next = {
206
260
  ...acta,
261
+ // Asciende de v1 a v2 sin ceremonia: los campos de sellado nacen vacíos y la llave
262
+ // entra en cuanto quien sella pase una (`sealPub`).
263
+ v: ACTA_V,
264
+ sealPub: acta.sealPub || null,
265
+ sealSince: acta.sealPub ? acta.sealSince : 0,
207
266
  sealedBy: by,
208
267
  seq: acta.seq + 1,
209
268
  prev: await actaHash(acta),
@@ -211,10 +270,24 @@ export async function applyChanges (acta, changes, { by, now = Date.now() } = {}
211
270
  revoked: [...(acta.revoked || [])],
212
271
  renounced: [...(acta.renounced || [])],
213
272
  keyring: (acta.keyring || []).map((g) => ({ ...g, wraps: { ...g.wraps } })),
273
+ sealKeys: (acta.sealKeys || []).map((k) => ({ ...k })),
214
274
  updatedAt: now
215
275
  }
216
276
  delete next.sig
217
277
 
278
+ // LA LLAVE DE SELLADO ROTA CON EL ACTA (§8.9 de `secretos-sellados.md`). Quien sella
279
+ // pasa la llave nueva; si no pasa ninguna, la de antes sigue mandando —un master que no
280
+ // sella secretos (el navegador) no tiene por qué inventarse una—.
281
+ //
282
+ // La anterior NO se tira: se guarda con el tramo de `seq` en el que estuvo en vigor,
283
+ // porque los sobres que firmó tienen que seguir verificando. Re-firmarlos al rotar
284
+ // sería recorrer todos los secretos en cada cambio de membresía.
285
+ if (sealPub && sealPub !== next.sealPub) {
286
+ if (next.sealPub) next.sealKeys.push({ pub: next.sealPub, from: next.sealSince, to: acta.seq })
287
+ next.sealPub = sealPub
288
+ next.sealSince = next.seq
289
+ }
290
+
218
291
  const find = (pub) => next.members.find((m) => m.pub === pub)
219
292
 
220
293
  for (const ch of list) {
@@ -239,6 +312,19 @@ export async function applyChanges (acta, changes, { by, now = Date.now() } = {}
239
312
  })
240
313
  break
241
314
  }
315
+ // Registra (o reemplaza) la llave de CIFRADO de un miembro que YA está admitido.
316
+ //
317
+ // Existe para no tener que expulsar y volver a admitir a un servicio solo porque
318
+ // le falta la llave: re-enrolar le cambia la pubkey, y con ella pierde su cajón de
319
+ // variables —que va indexado por esa llave— y se queda sin configuración en
320
+ // silencio. Registrar la llave en el sitio no toca ni `pub`, ni `cn`, ni `caps`.
321
+ case 'encpub': {
322
+ const m = find(ch.pub)
323
+ if (!m) throw new Error('encpub: that member is not in the record')
324
+ if (!isEncPub(ch.encPub)) throw new Error('encpub: invalid encryption key (expected a P-256 public JWK)')
325
+ m.encPub = ch.encPub
326
+ break
327
+ }
242
328
  case 'caps': {
243
329
  const m = find(ch.pub)
244
330
  if (!m) throw new Error('caps: that member is not in the record')
@@ -332,6 +418,19 @@ export async function applyChanges (acta, changes, { by, now = Date.now() } = {}
332
418
  return next
333
419
  }
334
420
 
421
+ /**
422
+ * La llave que firmaba los sobres cuando el acta iba por `seq`. Es lo que necesita quien
423
+ * VERIFICA un sobre: el sobre dice con qué acta se selló, y esto dice con qué llave hay
424
+ * que comprobarlo. `null` si no había ninguna (perfil que no sellaba) o si el `seq` viene
425
+ * del futuro, que es un sobre que no puede ser bueno.
426
+ */
427
+ export function sealKeyAt (acta, seq) {
428
+ if (!acta || !Number.isInteger(seq) || seq < 1 || seq > acta.seq) return null
429
+ if (acta.sealPub && seq >= acta.sealSince) return acta.sealPub
430
+ for (const k of acta.sealKeys || []) if (seq >= k.from && seq <= k.to) return k.pub
431
+ return null
432
+ }
433
+
335
434
  // ----- renuncia (§2.2): el único cambio que no pasa por el master -----
336
435
 
337
436
  /** Crea el registro firmado con el que un miembro se QUITA capacidades a sí mismo. */
@@ -24,6 +24,7 @@ import { canonicalStringify, bufToBase64, base64ToBuf } from './core.js'
24
24
  import { pubkeyId, keyLabel } from './keyid.js'
25
25
 
26
26
  const ECDSA = { name: 'ECDSA', namedCurve: 'P-256' }
27
+ const ECDH = { name: 'ECDH', namedCurve: 'P-256' }
27
28
  const SIGN = { name: 'ECDSA', hash: { name: 'SHA-256' } }
28
29
 
29
30
  /** Tope DURO de vida de una delegación (aunque pidan más). */
@@ -120,6 +121,44 @@ export async function makeDeviceKey ({ label = '' } = {}) {
120
121
  return { publickey, privateJwk, publicJwk, label: String(label || ''), createdAt: Date.now(), deviceId: await pubkeyId(publickey) }
121
122
  }
122
123
 
124
+ /**
125
+ * Genera la llave de CIFRADO del dispositivo (ECDH P-256): la hermana de
126
+ * `makeDeviceKey`, que es de FIRMA. Hacen falta las dos y no son intercambiables —
127
+ * con ECDSA no se puede cifrar.
128
+ *
129
+ * Es lo que permite escribirle a un aparato **por adelantado**, sin que esté
130
+ * conectado: su pública va al acta como `encPub` y cualquiera puede envolverle un
131
+ * secreto con `wrapForMember` (ver `./content.js`). La privada no sale de aquí.
132
+ *
133
+ * Sale EXTRAÍBLE porque un servicio headless la persiste en su archivo de identidad
134
+ * (cifrado en reposo). En el navegador la pareja del perfil vive como CryptoKey no
135
+ * extraíble en IndexedDB — eso lo hace `core.js`, no esto.
136
+ */
137
+ export async function makeDeviceEncKey () {
138
+ const pair = await crypto.subtle.generateKey(ECDH, true, ['deriveBits'])
139
+ const encPrivateJwk = await crypto.subtle.exportKey('jwk', pair.privateKey)
140
+ const encPublicJwk = await crypto.subtle.exportKey('jwk', pair.publicKey)
141
+ return {
142
+ encPublickey: JSON.stringify({ kty: encPublicJwk.kty, crv: encPublicJwk.crv, x: encPublicJwk.x, y: encPublicJwk.y }),
143
+ encPrivateJwk,
144
+ encPublicJwk,
145
+ createdAt: Date.now()
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Reconstruye la llave privada de cifrado desde su JWK. Es lo que hay que pasarle a
151
+ * `openWrap({ myEncPrivateKey })` para abrir un sobre dirigido a este aparato.
152
+ *
153
+ * Existe para que nadie la importe a mano: `deriveBits` es el ÚNICO uso correcto, y
154
+ * pedir otro (`deriveKey`) hace fallar la importación en Node con un error que no
155
+ * dice nada útil.
156
+ */
157
+ export async function importDeviceEncKey (encPrivateJwk) {
158
+ if (!encPrivateJwk || typeof encPrivateJwk !== 'object') throw new Error('importDeviceEncKey: falta el JWK de la privada')
159
+ return crypto.subtle.importKey('jwk', encPrivateJwk, ECDH, false, ['deriveBits'])
160
+ }
161
+
123
162
  /**
124
163
  * Firma un certificado de delegación con una `privateKey` (CryptoKey) cuyo pubkey
125
164
  * es `iss`. Lo usa el handler del vault (con la clave maestra). Devuelve el cert
package/vault/content.js CHANGED
@@ -111,6 +111,19 @@ export async function encryptWithCek ({ cek, gen, plaintext }) {
111
111
  return { gen, iv: b64(iv), ct: b64(ct) }
112
112
  }
113
113
 
114
+ /**
115
+ * Descifra un sobre teniendo YA la CEK. Es el inverso exacto de `encryptWithCek`.
116
+ *
117
+ * Existe para quien administra: tiene la clave a mano (la sacó de donde la guarde) y no
118
+ * necesita el llavero ni ser miembro. El camino normal —el del aparato que solo tiene su
119
+ * propia llave— es `decryptWithKeyring`.
120
+ */
121
+ export async function decryptWithCek ({ cek, envelope }) {
122
+ const k = await subtle.importKey('raw', fromB64(cek), { name: 'AES-GCM' }, false, ['decrypt'])
123
+ const pt = await subtle.decrypt({ name: 'AES-GCM', iv: fromB64(envelope.iv) }, k, fromB64(envelope.ct))
124
+ return new TextDecoder().decode(pt)
125
+ }
126
+
114
127
  /**
115
128
  * Descifra un sobre. Hay que darle el llavero porque el contenido viejo está cifrado con
116
129
  * generaciones anteriores: por eso las CEK antiguas se conservan (32 bytes cada una) en vez
@@ -128,5 +141,5 @@ export async function decryptWithKeyring ({ envelope, keyring, myPub, myEncPriva
128
141
 
129
142
  export default {
130
143
  makeContentKey, wrapForMember, openWrap, makeGeneration, myContentKey,
131
- encryptWithCek, decryptWithKeyring
144
+ encryptWithCek, decryptWithCek, decryptWithKeyring
132
145
  }
package/vault/core.js CHANGED
@@ -542,6 +542,13 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
542
542
  /** ¿Es ESTE dispositivo el master (el único que puede sellar)? */
543
543
  const amMaster = () => loadActa()?.sealer === publickeyJwkStr
544
544
 
545
+ /**
546
+ * Quien sella SOBRES (la bóveda) pone aquí una función que estrena una llave de sellado
547
+ * y devuelve su pública. Se llama en cada acta, porque esa llave rota con el acta.
548
+ * Un navegador no sella secretos: se queda en `null` y el acta no lleva llave.
549
+ */
550
+ let sealKeyProvider = null
551
+
545
552
  /** Sella con la llave del perfil (CryptoKey, puede ser no extractable). */
546
553
  const seal = (acta) => Acta.sealActa({ acta, privateKey: keypair.privateKey })
547
554
 
@@ -552,7 +559,15 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
552
559
  async function sealChanges (changes) {
553
560
  const acta = loadActa()
554
561
  if (!acta) throw new Error('this profile has no record yet')
555
- const next = await Acta.applyChanges(acta, changes, { by: publickeyJwkStr })
562
+ // LA LLAVE DE SELLADO ROTA CON EL ACTA (§8.9 de `secretos-sellados.md`): si quien usa
563
+ // esta identidad sella secretos —la bóveda—, le pedimos una llave nueva para nombrarla
564
+ // aquí. Si no hay proveedor, o falla, el acta sale igual con la llave de antes: no
565
+ // admitir un aparato porque no se pudo estrenar una llave sería el peor de los canjes.
566
+ let sealPub = null
567
+ if (sealKeyProvider) {
568
+ try { sealPub = await sealKeyProvider() } catch (_) { sealPub = null }
569
+ }
570
+ const next = await Acta.applyChanges(acta, changes, { by: publickeyJwkStr, sealPub })
556
571
  const sealed = await seal(next)
557
572
  pushHistory(acta) // la que deja de ser vigente entra en la ventana de retención
558
573
  saveActa(sealed)
@@ -1447,6 +1462,23 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1447
1462
  return { acta, isMaster: amMaster(), myCaps: Acta.effectiveCaps(acta, publickeyJwkStr, loadRenounces()) }
1448
1463
  },
1449
1464
 
1465
+ /**
1466
+ * Quien sella sobres de secretos (la bóveda) registra aquí cómo estrenar su LLAVE DE
1467
+ * SELLADO: una función que crea el par, se guarda la privada y devuelve la pública. Se
1468
+ * llama al sellar cada acta, que es cuando esa llave rota (§8.9).
1469
+ *
1470
+ * Es opt-in a propósito: una identidad de navegador no sella nada, y un acta sin llave
1471
+ * de sellado es perfectamente válida.
1472
+ */
1473
+ setSealKeyProvider (fn) {
1474
+ sealKeyProvider = typeof fn === 'function' ? fn : null
1475
+ },
1476
+
1477
+ /** La llave con la que se firmaron los sobres de un acta dada. Para VERIFICARLOS. */
1478
+ sealKeyAt (seq) {
1479
+ return Acta.sealKeyAt(loadActa(), seq)
1480
+ },
1481
+
1450
1482
  async profileMembers () {
1451
1483
  const acta = loadActa()
1452
1484
  if (!acta) return { members: [], profileId: null, seq: 0, sealer: null }
@@ -1460,7 +1492,13 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1460
1492
  caps: Acta.effectiveCaps(acta, m.pub, pend),
1461
1493
  addedAt: m.addedAt || null,
1462
1494
  isMe: m.pub === publickeyJwkStr,
1463
- isMaster: m.pub === acta.sealer
1495
+ isMaster: m.pub === acta.sealer,
1496
+ // La llave de CIFRADO del miembro y, derivado, si se le puede envolver algo.
1497
+ // Sale en la proyección porque sin ella ninguna interfaz (CLI, TUI, consola)
1498
+ // puede decir «a este aparato no se le puede escribir», y ese silencio es
1499
+ // justo el que hace que un secreto no llegue y nadie sepa por qué.
1500
+ encPub: m.encPub || null,
1501
+ canSeal: !!m.encPub
1464
1502
  })))
1465
1503
  return { members, profileId: acta.profileId, seq: acta.seq, sealer: acta.sealer, updatedAt: acta.updatedAt }
1466
1504
  },
@@ -1481,6 +1519,19 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1481
1519
 
1482
1520
  async isMaster () { return amMaster() },
1483
1521
 
1522
+ /**
1523
+ * Registra la llave de CIFRADO de un miembro ya admitido (solo el master).
1524
+ *
1525
+ * Es la alternativa a expulsarlo y volver a admitirlo: un servicio re-enrolado
1526
+ * estrena pubkey, y su cajón de variables va indexado por la vieja, así que
1527
+ * arrancaría sin configuración y sin decirlo. Esto le pone la llave sin moverle
1528
+ * nada más.
1529
+ */
1530
+ async setMemberEncPub ({ pub, encPub } = {}) {
1531
+ const acta = await sealChanges([{ op: 'encpub', pub, encPub }])
1532
+ return { ok: true, seq: acta.seq }
1533
+ },
1534
+
1484
1535
  /**
1485
1536
  * Admite un miembro (solo el master). El cert lo emite quien llama, antes o después.
1486
1537
  * `continuity`: si esa identidad ya existía por su cuenta, su puente firmado (F3).
package/src/types.d.ts DELETED
@@ -1,5 +0,0 @@
1
- interface Element { [key: string]: any; }
2
- interface EventTarget { [key: string]: any; }
3
- interface HTMLElement { [key: string]: any; }
4
- interface Event { [key: string]: any; }
5
- interface Window { [key: string]: any; }