@dotrino/identity 0.109.0 → 0.110.1

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.109.0",
3
+ "version": "0.110.1",
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/core.js CHANGED
@@ -1121,9 +1121,21 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
1121
1121
  // se hace al generar una propia — quién puede hacer qué con esta llave lo decide esta
1122
1122
  // casa, no una etiqueta que viajó dentro del paquete.
1123
1123
  const sinTopes = (jwk) => { const { key_ops: _o, ...resto } = jwk || {}; return resto }
1124
- await adoptJwkPair('sign', KEY_STORAGE, sinTopes(entrada.keys.sign), JSON.parse(entrada.publickey))
1125
- if (entrada.keys.enc && entrada.encPublickey) {
1126
- await adoptJwkPair('enc', ENC_KEY_STORAGE, sinTopes(entrada.keys.enc), JSON.parse(entrada.encPublickey))
1124
+ if (keyStore?.importPair) {
1125
+ // DENTRO DE UNA APP NATIVA: la llave que entrega la bóveda la guarda la app (cifrada
1126
+ // con una llave del chip) y firma ella. Van las dos mitades juntas, bajo un mismo id.
1127
+ // Si la bóveda no entrega llave de cifrado, la app estrena una (como hace el navegador).
1128
+ const traeEnc = !!(entrada.keys.enc && entrada.encPublickey)
1129
+ await keyStore.importPair({
1130
+ sign: { name: _scoped(KEY_STORAGE), privateJwk: sinTopes(entrada.keys.sign), publickey: entrada.publickey },
1131
+ enc: { name: _scoped(ENC_KEY_STORAGE), privateJwk: traeEnc ? sinTopes(entrada.keys.enc) : null, publickey: traeEnc ? entrada.encPublickey : null }
1132
+ })
1133
+ kv.removeItem(KEY_STORAGE); kv.removeItem(ENC_KEY_STORAGE)
1134
+ } else {
1135
+ await adoptJwkPair('sign', KEY_STORAGE, sinTopes(entrada.keys.sign), JSON.parse(entrada.publickey))
1136
+ if (entrada.keys.enc && entrada.encPublickey) {
1137
+ await adoptJwkPair('enc', ENC_KEY_STORAGE, sinTopes(entrada.keys.enc), JSON.parse(entrada.encPublickey))
1138
+ }
1127
1139
  }
1128
1140
  saveActa(entrada.acta)
1129
1141
  kv.setItem(ACTA_HISTORY_STORAGE, '[]')
@@ -1147,6 +1159,18 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
1147
1159
  list.push({ id: pid, name: entrada.user, pubkey: publickeyJwkStr })
1148
1160
  saveProfiles(list)
1149
1161
  emitVault({ phase: 'login', address: entrada.address, volatile: !remember })
1162
+ // En una app nativa, la cuenta pasa también a su pantalla de Pedidos, con el mismo papel
1163
+ // (como al emparejar). Si la app no la guarda, se dice: la entrada en sí ya está hecha.
1164
+ if (keypair?.privateKey?.external) {
1165
+ keypair.privateKey.paired({
1166
+ cert: entrada.cert, vault: entrada.iss, proxy: proxy || DEFAULT_PROXY,
1167
+ deviceId: await deviceIdDe(entrada.publickey),
1168
+ name: entrada.acta?.name || entrada.address, profileId: entrada.acta?.profileId || null
1169
+ }).then(
1170
+ () => emitVault({ phase: 'native-account', ok: true }),
1171
+ (e) => { console.warn('[identity] the phone did not save the account: ' + e.message); emitVault({ phase: 'native-account', ok: false, code: e.code || 'native-error', reason: e.message }) }
1172
+ )
1173
+ }
1150
1174
  return { id: pid, name: entrada.user, address: entrada.address, user: entrada.user, sid: entrada.sid, volatile: !remember }
1151
1175
  } catch (e) {
1152
1176
  // NADA DE MEDIAS CUENTAS: si algo falla, no se queda una identidad a medio adoptar en
@@ -78,6 +78,22 @@ export function withExternalKeys (keyStore, bridge) {
78
78
  const other = sib ? await keyStore.get(sib) : null
79
79
  if (rec?.external && !(other?.external === rec.external)) await bridge.call('remove', { kid: rec.external })
80
80
  },
81
+ /**
82
+ * UNA LLAVE QUE YA EXISTE (entrar con usuario y contraseña): la bóveda la entrega y no se
83
+ * puede meter en el chip, así que la app la guarda cifrada con una llave del chip y firma
84
+ * ella. Desde aquí es una llave externa más: la página no se queda con la privada.
85
+ * `sign` y `enc`: `{ name, privateJwk, publickey }`, con la pública TAL CUAL la escribe el
86
+ * acta (es la cadena con la que esa llave es miembro). Si la bóveda no entrega la de
87
+ * cifrado (`enc.privateJwk` nulo), la app estrena una y devuelve su pública.
88
+ */
89
+ async importPair ({ sign, enc }) {
90
+ const params = { sign: sign.privateJwk, publickey: sign.publickey }
91
+ if (enc.privateJwk) { params.enc = enc.privateJwk; params.encPub = enc.publickey }
92
+ const k = await bridge.call('import', params)
93
+ if (!k?.kid || !k.encPub) throw Object.assign(new Error('the phone did not keep the key'), { code: 'native-no-key' })
94
+ await keyStore.set(sign.name, { external: k.kid, kind: 'sign', publicJwk: JSON.parse(sign.publickey) })
95
+ await keyStore.set(enc.name, { external: k.kid, kind: 'enc', publicJwk: JSON.parse(enc.publickey || k.encPub) })
96
+ },
81
97
  async create (kind, name) {
82
98
  // Las dos mitades del perfil van bajo la MISMA llave del chip: si la otra ya nació
83
99
  // allí, se reutiliza; si no, se crea una nueva.
@@ -28,8 +28,9 @@ function plainKeyRecord (rec) {
28
28
  if (rec && typeof rec.external === 'string' && rec.publicJwk && (rec.kind === 'sign' || rec.kind === 'enc')) {
29
29
  return { external: rec.external, kind: rec.kind, publicJwk: rec.publicJwk }
30
30
  }
31
- // Una llave de SOFTWARE (adoptar un perfil, entrar con contraseña) no cabe en el chip, y
32
- // guardarla a medias sería perder la cuenta sin decirlo. Se para aquí y se dice.
31
+ // Una llave de SOFTWARE no se guarda aquí: guardarla a medias sería perder la cuenta sin
32
+ // decirlo. La de entrar con contraseña no llega a este punto: la guarda la app, cifrada con
33
+ // una llave del chip (`importPair` de `externalKeys.js`), y aquí queda su registro externo.
33
34
  throw Object.assign(new Error('this app keeps keys in the phone chip only: a software key cannot be stored here'), { code: 'native-no-import' })
34
35
  }
35
36
 
@@ -1,4 +1,4 @@
1
- Copia vendorizada de @dotrino/proxy-client@0.30.1 (dotrino-proxy-client/src/{index,client,signature,canonical,sealing,encpub,webrtc,stats,stats-log}.js).
1
+ Copia vendorizada de @dotrino/proxy-client@0.30.4 (dotrino-proxy-client/src/{index,client,signature,canonical,sealing,encpub,webrtc,stats,stats-log}.js).
2
2
  NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
3
3
  sealing.js resuelve @dotrino/identity/content de forma PEREZOSA (= ../../content.js
4
4
  por el import map): solo se carga si de verdad se sella algo.
@@ -37,16 +37,27 @@ const pairs = (prefix, o) => PATHS.map((k) => `${prefix}.${k}=${o[k]}`).join(' '
37
37
  */
38
38
  export function formatStats (s, opts = {}) {
39
39
  const tag = opts.label ? `[net] ${opts.label}` : '[net]'
40
+ // QUIÉN CONTESTÓ Y QUIÉN NO. Un sondeo (un ping a cada aparato del acta) deja una entrada
41
+ // por destinatario aunque nadie responda: esas no son conexiones y van resumidas
42
+ // (`unanswered`), igual que en el informe del topbar y de las apps nativas.
43
+ // `unknown`: quien nos escribió, nunca recibió respuesta y no sabemos su llave (otro aparato
44
+ // sondeando qué máquinas están encendidas). Tampoco es una conexión.
45
+ const isUnknown = (p) => p.msgsIn > 0 && !(p.msgsOut > 0) && !p.pubkey
46
+ const unknown = s.peers.filter(isUnknown)
47
+ const talking = s.peers.filter((p) => p.msgsIn > 0 && !isUnknown(p))
48
+ const silent = s.peers.filter((p) => !(p.msgsIn > 0))
40
49
  const routes = {}
41
- for (const p of s.peers) routes[p.route] = (routes[p.route] || 0) + 1
50
+ for (const p of talking) routes[p.route] = (routes[p.route] || 0) + 1
42
51
  const routeList = Object.keys(routes).sort().map((r) => `${r}=${routes[r]}`).join(',') || 'none'
43
52
  const lines = [
44
53
  `${tag} url=${s.url} connected=${s.connected ? 'yes' : 'no'} webrtc=${s.webrtc ? 'on' : 'off'}` +
45
54
  ` ws.in=${s.proxy.bytesIn} ws.out=${s.proxy.bytesOut}` +
46
55
  ` ${pairs('in', sum(s.peers, 'bytesIn'))} ${pairs('out', sum(s.peers, 'bytesOut'))}` +
47
- ` peers=${s.peers.length} routes=${routeList}`
56
+ ` peers=${talking.length} routes=${routeList}` +
57
+ (silent.length ? ` unanswered=${silent.length} unanswered.out=${silent.reduce((n, p) => n + PATHS.reduce((m, k) => m + p.bytesOut[k], 0), 0)}` : '') +
58
+ (unknown.length ? ` unknown=${unknown.length} unknown.in=${unknown.reduce((n, p) => n + PATHS.reduce((m, k) => m + p.bytesIn[k], 0), 0)}` : '')
48
59
  ]
49
- for (const p of s.peers) {
60
+ for (const p of talking) {
50
61
  lines.push(
51
62
  `${tag} peer=${p.token || 'by-key'} route=${p.route}` +
52
63
  ` ${pairs('in', p.bytesIn)} ${pairs('out', p.bytesOut)} msgs.in=${p.msgsIn} msgs.out=${p.msgsOut}`
@@ -1,4 +1,4 @@
1
- Copia vendorizada de @dotrino/vault@0.80.0 (dotrino-vault/lib/src/{index,enroll,protocol,passwordLogins,loginClient,b64}.js).
1
+ Copia vendorizada de @dotrino/vault@0.82.1 (dotrino-vault/lib/src/{index,enroll,protocol,passwordLogins,loginClient,b64}.js).
2
2
  NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
3
3
  index.js importa ./enroll.js y ./protocol.js (relativos, van en esta misma copia),
4
4
  @dotrino/identity/{capabilities,acta} (= ../../{capabilities,acta}.js) y