@dotrino/identity 0.11.0 → 0.12.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.11.0",
3
+ "version": "0.12.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/src/index.js CHANGED
@@ -245,6 +245,15 @@ export class Identity {
245
245
  return this._call('listVaultDevices', {}, 20000)
246
246
  }
247
247
 
248
+ /**
249
+ * El cert de delegación de este dispositivo (o null si no está emparejado). El
250
+ * transporte lo presenta al proxy en `identify` → "una identidad": el proxy enruta
251
+ * los mensajes dirigidos a tu maestra M también a este dispositivo. No tiene secretos.
252
+ */
253
+ async getVaultCert () {
254
+ return this._call('getVaultCert')
255
+ }
256
+
248
257
  /** Suscribe a eventos de emparejamiento ('vault'): { phase:'challenge'|'paired'|'unpaired', ... }. */
249
258
  onVault (handler) {
250
259
  return this.on('vault', handler)
package/src/node.js CHANGED
@@ -142,6 +142,7 @@ export class Identity {
142
142
  vaultSign (payload) { return this._h('vaultSign', { payload }) }
143
143
  vaultStore (method, args) { return this._h('vaultStore', { method, args }) }
144
144
  listVaultDevices () { return this._h('listVaultDevices') }
145
+ getVaultCert () { return this._h('getVaultCert') }
145
146
  onVault (handler) { return this.on('vault', handler) }
146
147
  mergeEndorsements (subject, endorsements, askerPubkey) {
147
148
  return this._h('mergeEndorsements', { subject, endorsements, askerPubkey })
package/vault/core.js CHANGED
@@ -515,7 +515,11 @@ export async function createIdentityCore ({ kv, peers, makeSync = null }) {
515
515
  // Genera D aquí dentro (su privada NUNCA sale de la identidad), hace el enroll
516
516
  // endurecido por el proxy y guarda el cert. NO cambia signData todavía (Fase 2).
517
517
  async vaultPair ({ qr }) {
518
- const res = await remoteEnroll({ qr, onChallenge: (c) => emitVault({ phase: 'challenge', deviceId: c.deviceId, sas: c.sas }) })
518
+ // Usa la PROPIA llave de identidad de este navegador como dispositivo: el cert delega
519
+ // TU identidad (P) desde la maestra M → una sola identidad (signData/identify/cert = P).
520
+ let device
521
+ try { const k = JSON.parse(kv.getItem(KEY_STORAGE)); device = { publickey: JSON.stringify(k.publicJwk), privateJwk: k.privateJwk } } catch (_) { device = undefined }
522
+ const res = await remoteEnroll({ qr, device, onChallenge: (c) => emitVault({ phase: 'challenge', deviceId: c.deviceId, sas: c.sas }) })
519
523
  kv.setItem(VAULT_DEVICE_STORAGE, JSON.stringify(res.device))
520
524
  kv.setItem(VAULT_CERT_STORAGE, JSON.stringify({ cert: res.cert, master: res.master, proxy: res.proxy, deviceId: res.deviceId, pairedAt: Date.now() }))
521
525
  emitVault({ phase: 'paired', deviceId: res.deviceId, master: res.master })
@@ -559,6 +563,13 @@ export async function createIdentityCore ({ kv, peers, makeSync = null }) {
559
563
  return remoteDevices({ master: v.master, proxy: v.proxy, device, cert: v.cert })
560
564
  },
561
565
 
566
+ // El cert de delegación de este dispositivo (para presentarlo al proxy en `identify`
567
+ // → "una identidad": el proxy bindea tu pubkey también bajo tu maestra M). Sin secretos.
568
+ async getVaultCert () {
569
+ const v = loadVaultCert()
570
+ return v?.cert ? { cert: v.cert, master: v.master } : null
571
+ },
572
+
562
573
  async listContacts () {
563
574
  return Object.values(loadPeers()).filter(p => p && p.isContact).sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0))
564
575
  },
package/vault/remote.js CHANGED
@@ -29,17 +29,20 @@ const MSG = {
29
29
  * @param {number} [opts.approveTimeoutMs] Espera de la aprobación humana (def 3 min).
30
30
  * @returns {Promise<{device, cert, master:string, proxy:string, deviceId:string}>}
31
31
  */
32
- export async function enrollDevice ({ qr, onChallenge, label = '', approveTimeoutMs = 180000 } = {}) {
32
+ export async function enrollDevice ({ qr, device, onChallenge, label = '', approveTimeoutMs = 180000 } = {}) {
33
33
  if (!qr?.iss || !qr?.proxy || !qr?.token || !qr?.sn) throw new Error('qr inválido (v2): faltan iss/proxy/token/sn')
34
34
  const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
35
35
  const client = new WebSocketProxyClient({ url: qr.proxy, enableWebRTC: false, autoReconnect: false })
36
36
  await client.connect()
37
37
  try {
38
- const device = await makeDeviceKey({ label })
39
- const deviceId = (await pubkeyId(device.publickey)).slice(0, 8).toUpperCase().replace(/(.{4})(.{4})/, '$1-$2')
40
- const sas = await deriveSAS(qr.iss, device.publickey, qr.sn)
41
- const data = { op: 'enroll', dpub: device.publickey, token: qr.token, sn: qr.sn, label, ts: Date.now() }
42
- const { signature } = await signWithDevice({ privateJwk: device.privateJwk, data })
38
+ // Por defecto genera una sub-clave nueva; pero el iframe pasa SU PROPIA llave de
39
+ // identidad (P) como `device` → el cert delega tu identidad y hay UNA sola (signData/
40
+ // identify/cert son la misma P).
41
+ const dev = device || await makeDeviceKey({ label })
42
+ const deviceId = (await pubkeyId(dev.publickey)).slice(0, 8).toUpperCase().replace(/(.{4})(.{4})/, '$1-$2')
43
+ const sas = await deriveSAS(qr.iss, dev.publickey, qr.sn)
44
+ const data = { op: 'enroll', dpub: dev.publickey, token: qr.token, sn: qr.sn, label, ts: Date.now() }
45
+ const { signature } = await signWithDevice({ privateJwk: dev.privateJwk, data })
43
46
 
44
47
  const enrolled = new Promise((resolve, reject) => {
45
48
  const off = client.on('message', (_from, p) => {
@@ -55,11 +58,11 @@ export async function enrollDevice ({ qr, onChallenge, label = '', approveTimeou
55
58
  const res = await enrolled
56
59
 
57
60
  // Validación estricta antes de guardar (cierra inyección de cert / sustitución de maestra).
58
- const v = await verifyDelegation({ cert: res.cert, expectedSub: device.publickey })
61
+ const v = await verifyDelegation({ cert: res.cert, expectedSub: dev.publickey })
59
62
  if (!v.ok) throw new Error('cert inválido: ' + v.reason)
60
63
  if (res.cert.iss !== qr.iss) throw new Error('cert firmado por una maestra distinta a la que viste')
61
- if (res.cert.sub !== device.publickey) throw new Error('cert emitido para otro dispositivo')
62
- return { device, cert: res.cert, master: qr.iss, proxy: qr.proxy, deviceId }
64
+ if (res.cert.sub !== dev.publickey) throw new Error('cert emitido para otro dispositivo')
65
+ return { device: dev, cert: res.cert, master: qr.iss, proxy: qr.proxy, deviceId }
63
66
  } finally { try { client.close() } catch (_) {} }
64
67
  }
65
68
 
@@ -1,4 +1,2 @@
1
- Copia vendorizada de @dotrino/proxy-client@0.6.3 (src/, sin dependencias).
2
- El iframe id.dotrino.com se sirve estático (sin bundler), así que el transporte
3
- va self-hosted aquí (no por CDN) para no depender de terceros en runtime.
4
- Actualizar: re-copiar node_modules/@dotrino/proxy-client/src/*.js y bumpear esta nota.
1
+ Copia vendorizada de @dotrino/proxy-client@0.6.4 (src/, sin dependencias).
2
+ Ver dotrino-identity: el iframe se sirve estatico, transporte self-hosted (no CDN).
@@ -233,9 +233,11 @@ export class WebSocketProxyClient {
233
233
  * (típicamente por el identity vault). Devuelve la respuesta del proxy con
234
234
  * `queued_delivered` (mensajes offline despachados al instante).
235
235
  */
236
- identify ({ data, signature }) {
236
+ identify ({ data, signature, cert }) {
237
237
  if (!data || !signature) throw new Error('identify requires {data, signature}')
238
- return this._request({ type: 'identify', data, signature }, 'identified')
238
+ const msg = { type: 'identify', data, signature }
239
+ if (cert) msg.cert = cert // "una identidad": el proxy bindea este token también bajo tu maestra M
240
+ return this._request(msg, 'identified')
239
241
  }
240
242
 
241
243
  /**