@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 +1 -1
- package/src/index.js +9 -0
- package/src/node.js +1 -0
- package/vault/core.js +12 -1
- package/vault/remote.js +12 -9
- package/vault/vendor/proxy-client/VERSION.txt +2 -4
- package/vault/vendor/proxy-client/client.js +4 -2
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
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:
|
|
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 !==
|
|
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.
|
|
2
|
-
|
|
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
|
-
|
|
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
|
/**
|