@dotrino/identity 0.114.1 → 0.115.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 +4 -1
- package/src/index.js +9 -1
- package/vault/acta.js +5 -1
- package/vault/core.js +29 -8
- package/vault/machine.js +95 -0
- package/vault/remote.js +9 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/identity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.115.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",
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
},
|
|
23
23
|
"./vault/core.js": "./vault/core.js",
|
|
24
24
|
"./vault/remote.js": "./vault/remote.js",
|
|
25
|
+
"./machine": {
|
|
26
|
+
"import": "./vault/machine.js"
|
|
27
|
+
},
|
|
25
28
|
"./acta": {
|
|
26
29
|
"import": "./vault/acta.js"
|
|
27
30
|
},
|
package/src/index.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* use this library share identity even across different origins.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { browserMachine } from '../vault/machine.js'
|
|
11
|
+
|
|
10
12
|
const DEFAULT_VAULT_URL = 'https://id.dotrino.com/'
|
|
11
13
|
|
|
12
14
|
let singleton = null
|
|
@@ -125,6 +127,9 @@ export class Identity {
|
|
|
125
127
|
this._me = msg.me || null
|
|
126
128
|
this._locked = !!msg.locked
|
|
127
129
|
this._readyResolve(this)
|
|
130
|
+
// Dónde corre esta página (`vault/machine.js`), para los pedidos a la bóveda. Un
|
|
131
|
+
// iframe viejo no conoce el método: no pasa nada, solo no se manda.
|
|
132
|
+
this._call('setMachine', { machine: browserMachine() }).catch(() => {})
|
|
128
133
|
return
|
|
129
134
|
}
|
|
130
135
|
|
|
@@ -427,7 +432,9 @@ export class Identity {
|
|
|
427
432
|
* @returns {Promise<{ ok:boolean, deviceId:string, master:string, exp:number, scope:string[] }>}
|
|
428
433
|
*/
|
|
429
434
|
async enrollDevice (qr, { label = '', join = 'current' } = {}) {
|
|
430
|
-
|
|
435
|
+
// DÓNDE CORRE se mira AQUÍ, en la página, y no dentro del iframe: el iframe no ve si la
|
|
436
|
+
// página es una extensión o la app nativa (`vault/machine.js`).
|
|
437
|
+
return this._call('vaultPair', { qr, label, join, machine: browserMachine() }, 200000)
|
|
431
438
|
}
|
|
432
439
|
|
|
433
440
|
/** Estado de emparejamiento: { paired, deviceId?, master?, scope?, exp?, pairedAt? }. */
|
|
@@ -853,4 +860,5 @@ export { makeDeviceKey, makeDeviceEncKey, importDeviceEncKey, signWithDevice, ve
|
|
|
853
860
|
// PARA QUIÉN vale una firma, y hasta cuándo. Verificar NO necesita el iframe ni la clave
|
|
854
861
|
// de nadie, así que un servicio puede importarlo suelto (`@dotrino/identity/assertion`, que
|
|
855
862
|
// no arrastra el cliente del vault); aquí se reexporta para quien ya tiene esto cargado.
|
|
863
|
+
export { cleanMachine, machineLine, browserMachine, MACHINE_KINDS } from '../vault/machine.js'
|
|
856
864
|
export { verifyAssertion, verifySignedFor, newAssertionNonce, cleanScopes, claimsAllowed, assertionBody, SCOPES, SCOPE_CLAIMS, ASSERTION_MAX_TTL_MS, ASSERTION_DEFAULT_TTL_MS, ASSERTION_MAX_SKEW_MS } from '../vault/assertion.js'
|
package/vault/acta.js
CHANGED
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import { canonicalStringify } from './core.js'
|
|
38
38
|
import { signWithDevice, verifyDeviceSig, verifyDelegation, pubkeyId } from './capabilities.js'
|
|
39
|
+
import { cleanMachine } from './machine.js'
|
|
40
|
+
export { cleanMachine }
|
|
39
41
|
|
|
40
42
|
export const ACTA_V = 5
|
|
41
43
|
|
|
@@ -816,7 +818,9 @@ export async function applyChanges (acta, changes, { by, now = Date.now(), sealP
|
|
|
816
818
|
addedAt: now,
|
|
817
819
|
cert: m.cert || null,
|
|
818
820
|
// Puente con la identidad que este miembro traía de antes (ver makeContinuity).
|
|
819
|
-
...(m.continuity ? { continuity: m.continuity } : {})
|
|
821
|
+
...(m.continuity ? { continuity: m.continuity } : {}),
|
|
822
|
+
// DÓNDE CORRE (`machine.js`): lo que dijo al emparejar, para reconocerlo en la lista.
|
|
823
|
+
...(cleanMachine(m.machine) ? { machine: cleanMachine(m.machine) } : {})
|
|
820
824
|
})
|
|
821
825
|
break
|
|
822
826
|
}
|
package/vault/core.js
CHANGED
|
@@ -791,7 +791,16 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
791
791
|
const encKeyId = async (encPub) => (await pubkeyIdOf(encPub)).slice(0, 16)
|
|
792
792
|
|
|
793
793
|
const loadVaultCert = () => { try { return JSON.parse(kv.getItem(VAULT_CERT_STORAGE) || 'null') } catch (_) { return null } }
|
|
794
|
+
// DÓNDE CORRE ESTE APARATO (`machine.js`). Lo dice la PÁGINA al conectar (`setMachine`),
|
|
795
|
+
// porque el iframe no ve si está dentro de una extensión o de la app nativa, y viaja con
|
|
796
|
+
// cada pedido firmado a la bóveda para que su lista lo enseñe al día. Sin página que lo
|
|
797
|
+
// diga (Node, un cliente viejo) no se manda nada: mejor ningún dato que uno equivocado.
|
|
798
|
+
let pageMachine = null
|
|
794
799
|
const loadVaultDevice = () => {
|
|
800
|
+
const d = loadVaultDeviceKey()
|
|
801
|
+
return d && pageMachine ? { ...d, machine: pageMachine } : d
|
|
802
|
+
}
|
|
803
|
+
const loadVaultDeviceKey = () => {
|
|
795
804
|
try {
|
|
796
805
|
const d = JSON.parse(kv.getItem(VAULT_DEVICE_STORAGE) || 'null')
|
|
797
806
|
if (!d) return null
|
|
@@ -1293,7 +1302,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
1293
1302
|
* cert contra la bóveda y entra a su cuenta. Aparte para que la decisión de CUÁL cuenta y
|
|
1294
1303
|
* el deshacerla si esto falla vivan juntos arriba, y aquí solo quede el trámite.
|
|
1295
1304
|
*/
|
|
1296
|
-
async function pairWithVault ({ qr, label = '' }) {
|
|
1305
|
+
async function pairWithVault ({ qr, label = '', machine = undefined }) {
|
|
1297
1306
|
// Usa la PROPIA llave de identidad de este navegador como dispositivo: el cert delega
|
|
1298
1307
|
// TU identidad (P) desde la maestra M → una sola identidad (signData/identify/cert = P).
|
|
1299
1308
|
// La privada es la CryptoKey del perfil (no extractable): se pasa como `privateKey`
|
|
@@ -1307,7 +1316,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
1307
1316
|
const continuity = (mine && mine.members.length === 1 && !isPendingJoin())
|
|
1308
1317
|
? await Acta.makeContinuity({ member: publickeyJwkStr, from: mine.profileId, privateKey: masterKey() })
|
|
1309
1318
|
: null
|
|
1310
|
-
const res = await remoteEnroll({ qr, device, continuity, encPub: encPublickeyJwkStr, label: label || me?.nickname || '', onChallenge: (c) => emitVault({ phase: 'challenge', deviceId: c.deviceId, code: c.code }) })
|
|
1319
|
+
const res = await remoteEnroll({ qr, device, continuity, encPub: encPublickeyJwkStr, label: label || me?.nickname || '', machine: machine === undefined && pageMachine ? pageMachine : machine, onChallenge: (c) => emitVault({ phase: 'challenge', deviceId: c.deviceId, code: c.code }) })
|
|
1311
1320
|
kv.setItem(VAULT_DEVICE_STORAGE, JSON.stringify({ useIdentityKey: true, publickey: publickeyJwkStr }))
|
|
1312
1321
|
kv.setItem(VAULT_CERT_STORAGE, JSON.stringify({ cert: res.cert, master: res.master, proxy: res.proxy, deviceId: res.deviceId, pairedAt: Date.now() }))
|
|
1313
1322
|
// Conectarse a una bóveda es ENTRAR A SU CUENTA: el acta viene con el cert.
|
|
@@ -2560,6 +2569,8 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
2560
2569
|
isService: !!m.cn,
|
|
2561
2570
|
caps: Acta.effectiveCaps(acta, m.pub, pend),
|
|
2562
2571
|
addedAt: m.addedAt || null,
|
|
2572
|
+
// Dónde corre, según dijo al emparejarse (`machine.js`). `null` si entró antes.
|
|
2573
|
+
machine: m.machine || null,
|
|
2563
2574
|
isMe: m.pub === publickeyJwkStr,
|
|
2564
2575
|
isMaster: Acta.canSeal(acta, m.pub),
|
|
2565
2576
|
// La llave de CIFRADO del miembro y, derivado, si se le puede envolver algo.
|
|
@@ -2605,7 +2616,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
2605
2616
|
* Admite un miembro (solo el master). El cert lo emite quien llama, antes o después.
|
|
2606
2617
|
* `continuity`: si esa identidad ya existía por su cuenta, su puente firmado (F3).
|
|
2607
2618
|
*/
|
|
2608
|
-
async admitMember ({ pub, encPub = null, label = '', cn = null, caps = null, cert = null, continuity = null } = {}) {
|
|
2619
|
+
async admitMember ({ pub, encPub = null, label = '', cn = null, caps = null, cert = null, continuity = null, machine = null } = {}) {
|
|
2609
2620
|
// `cn`: si viene, este miembro es un SERVICIO: abre el cajón de secretos de ESE nombre
|
|
2610
2621
|
// por llevarlo, sin permiso aparte, y no ve nada más del usuario. Sin `cn` es un dispositivo.
|
|
2611
2622
|
const finales = caps || (cn ? [] : ['store', 'read'])
|
|
@@ -2622,7 +2633,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
2622
2633
|
const mine = encPub ? await myCek() : null
|
|
2623
2634
|
if (mine) wrap = { op: 'wrap', gen: mine.gen, pub, wrap: await Content.wrapForMember({ cek: mine.cek, memberEncPub: encPub }) }
|
|
2624
2635
|
} catch (_) { wrap = null }
|
|
2625
|
-
const acta = await sealChanges([{ op: 'admit', member: { pub, encPub, label, cn, caps: finales, cert, continuity } }, ...(wrap ? [wrap] : [])])
|
|
2636
|
+
const acta = await sealChanges([{ op: 'admit', member: { pub, encPub, label, cn, caps: finales, cert, continuity, machine } }, ...(wrap ? [wrap] : [])])
|
|
2626
2637
|
return { ok: true, seq: acta.seq, wrapped: !!wrap }
|
|
2627
2638
|
},
|
|
2628
2639
|
|
|
@@ -2898,8 +2909,8 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
2898
2909
|
* master: si esta cuenta ya la manda otra bóveda, no hay nada que
|
|
2899
2910
|
* regalar y falla en voz alta.
|
|
2900
2911
|
*/
|
|
2901
|
-
async vaultPair ({ qr, label = '', join = 'current' }) {
|
|
2902
|
-
if (join === 'adopt') return handlers.vaultAdopt({ qr, label })
|
|
2912
|
+
async vaultPair ({ qr, label = '', join = 'current', machine = undefined }) {
|
|
2913
|
+
if (join === 'adopt') return handlers.vaultAdopt({ qr, label, machine })
|
|
2903
2914
|
// La cuenta abierta AQUÍ y la que se cree para este intento. `born` es la que hay que
|
|
2904
2915
|
// tirar si el emparejamiento no llega a término: nació para él y no tiene nada dentro.
|
|
2905
2916
|
const from = currentPid
|
|
@@ -2929,7 +2940,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
2929
2940
|
}
|
|
2930
2941
|
}
|
|
2931
2942
|
try {
|
|
2932
|
-
return await pairWithVault({ qr, label })
|
|
2943
|
+
return await pairWithVault({ qr, label, machine })
|
|
2933
2944
|
} catch (e) {
|
|
2934
2945
|
// El intento falló (código vencido, la bóveda dijo que no, se agotó la espera): la
|
|
2935
2946
|
// cuenta que nació para él se va con él. Si no, cada reintento dejaba una cuenta
|
|
@@ -2953,7 +2964,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
2953
2964
|
* bóveda, este aparato no puede regalar lo que no tiene; el traspaso se hace desde la
|
|
2954
2965
|
* que manda hoy.
|
|
2955
2966
|
*/
|
|
2956
|
-
async vaultAdopt ({ qr, label = '' } = {}) {
|
|
2967
|
+
async vaultAdopt ({ qr, label = '', machine = undefined } = {}) {
|
|
2957
2968
|
const mine = loadActa()
|
|
2958
2969
|
if (!mine) throw new Error('this device has no account to hand over yet')
|
|
2959
2970
|
if (!amMaster()) throw new Error('not-the-master: another device or vault is in charge of this account; the handover is done from there')
|
|
@@ -2963,6 +2974,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
2963
2974
|
qr,
|
|
2964
2975
|
device,
|
|
2965
2976
|
intent: 'adopt',
|
|
2977
|
+
machine,
|
|
2966
2978
|
profileId: mine.profileId,
|
|
2967
2979
|
encPub: encPublickeyJwkStr,
|
|
2968
2980
|
label: label || me?.nickname || '',
|
|
@@ -3009,6 +3021,15 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
3009
3021
|
return { ok: true, pending: true }
|
|
3010
3022
|
},
|
|
3011
3023
|
|
|
3024
|
+
/**
|
|
3025
|
+
* La página dice dónde corre (`machine.js`); se manda con los pedidos a la bóveda.
|
|
3026
|
+
* @param {any} [args]
|
|
3027
|
+
*/
|
|
3028
|
+
async setMachine ({ machine } = {}) {
|
|
3029
|
+
pageMachine = Acta.cleanMachine(machine)
|
|
3030
|
+
return { ok: true }
|
|
3031
|
+
},
|
|
3032
|
+
|
|
3012
3033
|
async vaultStatus () {
|
|
3013
3034
|
const v = loadVaultCert()
|
|
3014
3035
|
if (!v?.cert) return { paired: false }
|
package/vault/machine.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUÉ MÁQUINA ES CADA APARATO: dónde corre, y nada más.
|
|
3
|
+
*
|
|
4
|
+
* En la lista de aparatos de la bóveda solo había un ID (`AB12-CD34`), una etiqueta que pone
|
|
5
|
+
* el propio aparato —muchas veces el apodo del usuario, igual en todos— y la fecha de alta.
|
|
6
|
+
* Con tres navegadores y dos agentes no había forma de saber cuál era cuál (dueño,
|
|
7
|
+
* 2026-10-15). Esto es lo que lo distingue a simple vista:
|
|
8
|
+
*
|
|
9
|
+
* · `kind` — qué es: `node` (un agente o un comando), `browser`, `extension`, `ios`, `android`.
|
|
10
|
+
* · `os` — el sistema donde corre (`Linux 7.0.0`, `Windows`, `macOS`, `Android 14`).
|
|
11
|
+
* · `name` — en Node, el hostname; en un navegador, el navegador (`Chrome`). Un navegador no
|
|
12
|
+
* sabe el nombre de la máquina, y lo que no se sabe no se inventa.
|
|
13
|
+
*
|
|
14
|
+
* Lo DICE el aparato y va dentro de lo que firma, así que nadie más puede escribirlo por él.
|
|
15
|
+
* Tampoco es una prueba de nada: un aparato puede llamarse como quiera. Sirve para reconocerlo,
|
|
16
|
+
* no para decidir — ninguna puerta mira esto.
|
|
17
|
+
*
|
|
18
|
+
* Una sola pieza para las dos puntas: lo limpia igual quien lo manda y quien lo guarda (el acta,
|
|
19
|
+
* la bóveda), para que un campo de más o una cadena de un megabyte no entren por ningún lado.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const MACHINE_KINDS = ['node', 'browser', 'extension', 'ios', 'android']
|
|
23
|
+
|
|
24
|
+
const clip = (s, n) => (typeof s === 'string' ? s.replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, n) : '')
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Lo deja en la forma de siempre, o `null` si no dice nada que valga. Nunca lanza: un aparato
|
|
28
|
+
* viejo o raro no puede tumbar un emparejamiento por cómo describe su máquina.
|
|
29
|
+
* @param {any} raw
|
|
30
|
+
* @returns {{ kind: string, os: string, name: string } | null}
|
|
31
|
+
*/
|
|
32
|
+
export function cleanMachine (raw) {
|
|
33
|
+
if (!raw || typeof raw !== 'object') return null
|
|
34
|
+
const kind = MACHINE_KINDS.includes(raw.kind) ? raw.kind : ''
|
|
35
|
+
const os = clip(raw.os, 40)
|
|
36
|
+
const name = clip(raw.name, 64)
|
|
37
|
+
if (!kind && !os && !name) return null
|
|
38
|
+
return { kind, os, name }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Una línea para enseñarlo: `node · Linux 7.0.0 · mi-pc`. Vacía si no hay nada. */
|
|
42
|
+
export function machineLine (m) {
|
|
43
|
+
const c = cleanMachine(m)
|
|
44
|
+
return c ? [c.kind, c.os, c.name].filter(Boolean).join(' · ') : ''
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** ¿Dicen lo mismo? Para no reescribir un archivo cada vez que un aparato repite lo de siempre. */
|
|
48
|
+
export const sameMachine = (a, b) => machineLine(a) === machineLine(b)
|
|
49
|
+
|
|
50
|
+
/** @type {Array<[RegExp, (v: string) => string]>} */
|
|
51
|
+
const UA_OS = [
|
|
52
|
+
[/Android ([\d.]+)/, (v) => 'Android ' + v.split('.')[0]],
|
|
53
|
+
[/(?:iPhone|iPad|iPod).*? OS (\d+)/, (v) => 'iOS ' + v],
|
|
54
|
+
[/Windows NT/, () => 'Windows'],
|
|
55
|
+
[/CrOS/, () => 'ChromeOS'],
|
|
56
|
+
[/Mac OS X/, () => 'macOS'],
|
|
57
|
+
[/Linux/, () => 'Linux']
|
|
58
|
+
]
|
|
59
|
+
/** @type {Array<[RegExp, string]>} */
|
|
60
|
+
const UA_BROWSER = [
|
|
61
|
+
[/Edg\//, 'Edge'], [/OPR\//, 'Opera'], [/Firefox\//, 'Firefox'], [/SamsungBrowser\//, 'Samsung Internet'],
|
|
62
|
+
[/Chrome\//, 'Chrome'], [/Safari\//, 'Safari']
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* La máquina de ESTE navegador. `env` se inyecta en las pruebas; en la página es `globalThis`.
|
|
67
|
+
* Lo nativo se reconoce por el puente de `dotrino-native` (`DotrinoNative.platform()`), la
|
|
68
|
+
* extensión por tener `chrome.runtime.id`, y el resto es un navegador.
|
|
69
|
+
* @param {any} [env]
|
|
70
|
+
*/
|
|
71
|
+
export function browserMachine (env = globalThis) {
|
|
72
|
+
/** @type {any} */
|
|
73
|
+
const nav = env?.navigator || {}
|
|
74
|
+
const ua = String(nav.userAgent || '')
|
|
75
|
+
// Node también tiene `navigator` (`Node.js/22`): no es un navegador, y su máquina la dice
|
|
76
|
+
// quien sabe mirarla (`selfMachine` de `@dotrino/vault`). Aquí no se inventa.
|
|
77
|
+
if (env?.process?.versions?.node || !ua || ua.startsWith('Node.js')) return null
|
|
78
|
+
let kind = 'browser'
|
|
79
|
+
try {
|
|
80
|
+
const p = env?.DotrinoNative?.platform?.()
|
|
81
|
+
if (p === 'ios' || p === 'android') kind = p
|
|
82
|
+
else if (env?.chrome?.runtime?.id) kind = 'extension'
|
|
83
|
+
} catch (_) { /* un puente roto no cambia qué navegador es: se queda en `browser` */ }
|
|
84
|
+
let os = ''
|
|
85
|
+
for (const [re, fmt] of UA_OS) { const m = ua.match(re); if (m) { os = fmt(m[1] || ''); break } }
|
|
86
|
+
// iPadOS se presenta como un Mac de escritorio: lo delata la pantalla táctil.
|
|
87
|
+
if (os === 'macOS' && nav.maxTouchPoints > 1) os = 'iPadOS'
|
|
88
|
+
let name = ''
|
|
89
|
+
if (kind === 'browser' || kind === 'extension') {
|
|
90
|
+
for (const [re, n] of UA_BROWSER) if (re.test(ua)) { name = n; break }
|
|
91
|
+
}
|
|
92
|
+
return cleanMachine({ kind, os, name })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default { MACHINE_KINDS, cleanMachine, machineLine, sameMachine, browserMachine }
|
package/vault/remote.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { makeDeviceKey, signWithDevice, verifyDeviceSig, makePairingCode, commitCode, pubkeyId } from './capabilities.js'
|
|
17
17
|
import { checkVaultReply } from './acta.js'
|
|
18
|
+
import { cleanMachine, browserMachine } from './machine.js'
|
|
18
19
|
|
|
19
20
|
const MSG = {
|
|
20
21
|
HELLO: 'vault.hello',
|
|
@@ -130,7 +131,7 @@ async function askVault (client, qr) {
|
|
|
130
131
|
* @param {number} [opts.approveTimeoutMs] Espera de la aprobación humana (def 3 min).
|
|
131
132
|
* @returns {Promise<{device, cert, master:string, proxy:string, deviceId:string, account:string}>}
|
|
132
133
|
*/
|
|
133
|
-
export async function enrollDevice ({ qr, device, onChallenge, label = '', continuity = null, encPub = null, approveTimeoutMs = 180000, intent = 'join', profileId = null, onAdopt = null } = {}) {
|
|
134
|
+
export async function enrollDevice ({ qr, device, onChallenge, label = '', continuity = null, encPub = null, approveTimeoutMs = 180000, intent = 'join', profileId = null, onAdopt = null, machine = undefined } = {}) {
|
|
134
135
|
if (!qr?.sn || !(qr.iss || qr.conn)) throw new Error('invalid qr: missing vault or nonce')
|
|
135
136
|
const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
|
|
136
137
|
const client = new WebSocketProxyClient({ url: qr.proxy || 'wss://proxy.dotrino.com', enableWebRTC: false, autoReconnect: false })
|
|
@@ -162,12 +163,16 @@ export async function enrollDevice ({ qr, device, onChallenge, label = '', conti
|
|
|
162
163
|
// rechaza el que no coincida con el modo con el que ella abrió el emparejamiento. Así
|
|
163
164
|
// ninguno de los dos puede hacer, a mitad de camino, algo distinto de lo que el humano
|
|
164
165
|
// vio anunciado en las dos pantallas.
|
|
166
|
+
// DÓNDE CORRE (`machine.js`), para que la bóveda lo reconozca en su lista. Quien llama
|
|
167
|
+
// puede pasarlo (lo sabe mejor); si no, se mira el navegador. `null` es no decir nada.
|
|
168
|
+
const maquina = cleanMachine(machine === undefined ? browserMachine() : machine)
|
|
165
169
|
const adopting = intent === 'adopt'
|
|
166
170
|
if (adopting && typeof onAdopt !== 'function') throw new Error('enrollDevice(adopt): missing onAdopt')
|
|
167
171
|
const data = {
|
|
168
172
|
op: 'enroll', dpub: dev.publickey, token: qr.token || qr.sn, sn: qr.sn, commit, label, ts: Date.now(), intent,
|
|
169
173
|
...(adopting && profileId ? { profileId } : {}),
|
|
170
|
-
...(continuity ? { continuity } : {}), ...(encPub ? { encPub } : {})
|
|
174
|
+
...(continuity ? { continuity } : {}), ...(encPub ? { encPub } : {}),
|
|
175
|
+
...(maquina ? { machine: maquina } : {})
|
|
171
176
|
}
|
|
172
177
|
// `dev.sign`: la llave vive fuera (el chip del teléfono) y solo se le pide la firma.
|
|
173
178
|
const { signature } = await signWithDevice({ privateJwk: dev.privateJwk, privateKey: dev.privateKey, publickey: dev.publickey, sign: dev.sign, data })
|
|
@@ -283,7 +288,7 @@ async function vaultRpc ({ master, proxy, device, cert, acta = null, sendType, o
|
|
|
283
288
|
await client.connect()
|
|
284
289
|
try {
|
|
285
290
|
try { await identifyAsDevice(client, device, { cert, acta }) } catch (_) { /* sin identify seguimos: solo perdemos la cola */ }
|
|
286
|
-
const signed = { ...data, publickey: device.publickey, ts: Date.now() }
|
|
291
|
+
const signed = { ...data, publickey: device.publickey, ts: Date.now(), ...(cleanMachine(device.machine) ? { machine: cleanMachine(device.machine) } : {}) }
|
|
287
292
|
const { signature } = await signWithDevice({ privateJwk: device.privateJwk, privateKey: device.privateKey, publickey: device.publickey, sign: device.sign, data: signed })
|
|
288
293
|
const pending = new Promise((resolve, reject) => {
|
|
289
294
|
let graceTimer = null
|
|
@@ -338,7 +343,7 @@ export async function checkMembership ({ master, proxy, device, onRevoked, timeo
|
|
|
338
343
|
// Identificarse hace además que el proxy entregue lo ENCOLADO (un aviso de cuando
|
|
339
344
|
// estaba apagado, si todavía está dentro de las 24 h).
|
|
340
345
|
try { await identifyAsDevice(client, device) } catch (_) {}
|
|
341
|
-
const data = { op: 'check', publickey: device.publickey, ts: Date.now() }
|
|
346
|
+
const data = { op: 'check', publickey: device.publickey, ts: Date.now(), ...(cleanMachine(device.machine) ? { machine: cleanMachine(device.machine) } : {}) }
|
|
342
347
|
const { signature } = await signWithDevice({ privateJwk: device.privateJwk, privateKey: device.privateKey, publickey: device.publickey, sign: device.sign, data })
|
|
343
348
|
const res = await new Promise((resolve) => {
|
|
344
349
|
let settled = false
|