@dotrino/identity 0.24.0 → 0.25.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.24.0",
3
+ "version": "0.25.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",
package/vault/acta.js CHANGED
@@ -168,7 +168,9 @@ export async function applyChanges (acta, changes, { by, now = Date.now() } = {}
168
168
  label: String(m.label || '').slice(0, 60),
169
169
  caps: cleanCaps(m.caps),
170
170
  addedAt: now,
171
- cert: m.cert || null
171
+ cert: m.cert || null,
172
+ // Puente con la identidad que este miembro traía de antes (ver makeContinuity).
173
+ ...(m.continuity ? { continuity: m.continuity } : {})
172
174
  })
173
175
  break
174
176
  }
@@ -263,6 +265,30 @@ export function memberCan (acta, pub, cap, extraRenounces = []) {
263
265
  return effectiveCaps(acta, pub, extraRenounces).includes(cap)
264
266
  }
265
267
 
268
+ // ----- continuidad: unir una identidad que ya existía -----
269
+
270
+ /**
271
+ * CERTIFICADO DE CONTINUIDAD. Cuando una identidad que ya existía entra en otro perfil,
272
+ * firma —con su propia llave— que a partir de ahora es miembro de él. Sirve de puente:
273
+ * lo que hizo antes (su reputación, lo que firmó, quien la tenía de contacto) se puede
274
+ * seguir atribuyendo a la misma persona en vez de quedar huérfano.
275
+ *
276
+ * No otorga nada por sí solo: es una declaración del que se une, y solo tiene efecto
277
+ * dentro del acta donde el master la mete.
278
+ */
279
+ export async function makeContinuity ({ member, from, privateKey, privateJwk, now = Date.now() }) {
280
+ const body = { op: 'continuity', member, from: from || member, ts: now }
281
+ const { signature } = await signWithDevice({ privateKey, privateJwk, publickey: member, data: body })
282
+ return { ...body, sig: signature }
283
+ }
284
+
285
+ /** ¿La firmó de verdad la identidad que dice venir? (única comprobación posible). */
286
+ export async function verifyContinuity (record) {
287
+ if (!record || record.op !== 'continuity' || !isPub(record.member) || typeof record.sig !== 'string') return false
288
+ const { sig, ...body } = record
289
+ return verifyDeviceSig({ publickey: record.member, data: body, signature: sig })
290
+ }
291
+
266
292
  // ----- adopción y empates (§2.4.1) -----
267
293
 
268
294
  /**
@@ -311,5 +337,6 @@ export async function canAdopt ({ candidate, current }) {
311
337
  export default {
312
338
  ACTA_V, CAPS, CAP_SCOPE, genesisActa, actaBody, actaHash, memberId, checkShape, isHandover,
313
339
  sealActa, verifyActa, applyChanges, makeRenounce, verifyRenounce,
340
+ makeContinuity, verifyContinuity,
314
341
  effectiveCaps, memberCan, canAdopt
315
342
  }
package/vault/core.js CHANGED
@@ -1096,7 +1096,13 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1096
1096
  // La privada es la CryptoKey del perfil (no extractable): se pasa como `privateKey`
1097
1097
  // y NO se persiste ningún JWK del dispositivo (marcador useIdentityKey).
1098
1098
  const device = { publickey: publickeyJwkStr, privateKey: keypair.privateKey }
1099
- const res = await remoteEnroll({ qr, device, onChallenge: (c) => emitVault({ phase: 'challenge', deviceId: c.deviceId, code: c.code }) })
1099
+ // Si esta identidad ya existía por su cuenta, se lleva un certificado de continuidad
1100
+ // firmado por ella misma: es el puente para que su reputación previa siga contando.
1101
+ const mio = loadActa()
1102
+ const continuity = (mio && mio.members.length === 1)
1103
+ ? await Acta.makeContinuity({ member: publickeyJwkStr, from: mio.profileId, privateKey: keypair.privateKey })
1104
+ : null
1105
+ const res = await remoteEnroll({ qr, device, continuity, onChallenge: (c) => emitVault({ phase: 'challenge', deviceId: c.deviceId, code: c.code }) })
1100
1106
  kv.setItem(VAULT_DEVICE_STORAGE, JSON.stringify({ useIdentityKey: true, publickey: publickeyJwkStr }))
1101
1107
  kv.setItem(VAULT_CERT_STORAGE, JSON.stringify({ cert: res.cert, master: res.master, proxy: res.proxy, deviceId: res.deviceId, pairedAt: Date.now() }))
1102
1108
  // Conectarse a una bóveda es ENTRAR A SU PERFIL: el acta viene con el cert.
package/vault/remote.js CHANGED
@@ -56,7 +56,7 @@ async function identifyAsDevice (client, device) {
56
56
  * @param {number} [opts.approveTimeoutMs] Espera de la aprobación humana (def 3 min).
57
57
  * @returns {Promise<{device, cert, master:string, proxy:string, deviceId:string}>}
58
58
  */
59
- export async function enrollDevice ({ qr, device, onChallenge, label = '', approveTimeoutMs = 180000 } = {}) {
59
+ export async function enrollDevice ({ qr, device, onChallenge, label = '', continuity = null, approveTimeoutMs = 180000 } = {}) {
60
60
  if (!qr?.iss || !qr?.proxy || !qr?.token || !qr?.sn) throw new Error('qr inválido (v2): faltan iss/proxy/token/sn')
61
61
  const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
62
62
  const client = new WebSocketProxyClient({ url: qr.proxy, enableWebRTC: false, autoReconnect: false })
@@ -75,7 +75,9 @@ export async function enrollDevice ({ qr, device, onChallenge, label = '', appro
75
75
  // haber leído el código de ESTA pantalla. Y al ECHARLO de vuelta, el dispositivo confía:
76
76
  // una bóveda falsa no conoce el código y no puede enrolarlo.
77
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() }
78
+ // `continuity`: si esta identidad ya existía, va firmada por ella misma para que lo
79
+ // que hizo antes se pueda seguir atribuyendo a la misma persona (ver acta.js).
80
+ const data = { op: 'enroll', dpub: dev.publickey, token: qr.token, sn: qr.sn, commit, label, ts: Date.now(), ...(continuity ? { continuity } : {}) }
79
81
  const { signature } = await signWithDevice({ privateJwk: dev.privateJwk, privateKey: dev.privateKey, publickey: dev.publickey, data })
80
82
 
81
83
  const enrolled = new Promise((resolve, reject) => {
@@ -1,4 +1,4 @@
1
- Copia vendorizada de @dotrino/vault@0.5.0 (lib/src/{index,enroll}.js, sin dependencias).
1
+ Copia vendorizada de @dotrino/vault@0.6.0 (lib/src/{index,enroll}.js, sin dependencias).
2
2
  El iframe de identity se sirve estatico (vanilla, sin build); asi startDeviceVault
3
3
  resuelve en el navegador sin bundler. index.js importa ./enroll.js (relativo, se
4
4
  vendoriza tambien) y @dotrino/identity/capabilities (=../../capabilities.js) y
@@ -30,6 +30,7 @@
30
30
  * `docs/pairing-protocol.md`).
31
31
  */
32
32
  import { verifyDeviceSig, pubkeyId, commitCode } from '@dotrino/identity/capabilities'
33
+ import { verifyContinuity } from '@dotrino/identity/acta'
33
34
 
34
35
  /** Un token de emparejamiento vale 5 min. */
35
36
  export const PAIRING_TTL_MS = 5 * 60 * 1000
@@ -157,6 +158,12 @@ export function createEnrollDesk ({
157
158
  pend.dpub = d.dpub
158
159
  pend.deviceId = deviceId
159
160
  pend.commit = d.commit
161
+ // Certificado de continuidad (opcional): lo firma la identidad que se une, con su
162
+ // propia llave. Se comprueba aquí y se guarda con el miembro al aprobar.
163
+ if (d.continuity) {
164
+ const okC = await verifyContinuity(d.continuity)
165
+ pend.continuity = (okC && d.continuity.member === d.dpub) ? d.continuity : null
166
+ }
160
167
  pend.from = from // la bóveda NO conoce el código: lo aprende cuando lo tipeas
161
168
  if (d.label) pend.label = String(d.label).slice(0, 60)
162
169
 
@@ -206,7 +213,7 @@ export function createEnrollDesk ({
206
213
  try {
207
214
  if (typeof identity.admitMember === 'function') {
208
215
  const caps = scopeToCaps(pend.scope)
209
- if (caps.length) await identity.admitMember({ pub: pend.dpub, label: pend.label || '', caps, cert })
216
+ if (caps.length) await identity.admitMember({ pub: pend.dpub, label: pend.label || '', caps, cert, continuity: pend.continuity || null })
210
217
  }
211
218
  acta = (await identity.profileActa?.())?.acta || null
212
219
  } catch (e) { log('[vault] no se pudo admitir en el acta:', e.message) }