@dotrino/identity 0.109.0 → 0.110.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.109.0",
3
+ "version": "0.110.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/core.js CHANGED
@@ -1121,9 +1121,22 @@ 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
+ if (!entrada.keys.enc || !entrada.encPublickey) {
1128
+ throw Object.assign(new Error('this login has no encryption key: the phone needs both'), { code: 'native-import-needs-enc' })
1129
+ }
1130
+ await keyStore.importPair({
1131
+ sign: { name: _scoped(KEY_STORAGE), privateJwk: sinTopes(entrada.keys.sign), publickey: entrada.publickey },
1132
+ enc: { name: _scoped(ENC_KEY_STORAGE), privateJwk: sinTopes(entrada.keys.enc), publickey: entrada.encPublickey }
1133
+ })
1134
+ kv.removeItem(KEY_STORAGE); kv.removeItem(ENC_KEY_STORAGE)
1135
+ } else {
1136
+ await adoptJwkPair('sign', KEY_STORAGE, sinTopes(entrada.keys.sign), JSON.parse(entrada.publickey))
1137
+ if (entrada.keys.enc && entrada.encPublickey) {
1138
+ await adoptJwkPair('enc', ENC_KEY_STORAGE, sinTopes(entrada.keys.enc), JSON.parse(entrada.encPublickey))
1139
+ }
1127
1140
  }
1128
1141
  saveActa(entrada.acta)
1129
1142
  kv.setItem(ACTA_HISTORY_STORAGE, '[]')
@@ -1147,6 +1160,18 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
1147
1160
  list.push({ id: pid, name: entrada.user, pubkey: publickeyJwkStr })
1148
1161
  saveProfiles(list)
1149
1162
  emitVault({ phase: 'login', address: entrada.address, volatile: !remember })
1163
+ // En una app nativa, la cuenta pasa también a su pantalla de Pedidos, con el mismo papel
1164
+ // (como al emparejar). Si la app no la guarda, se dice: la entrada en sí ya está hecha.
1165
+ if (keypair?.privateKey?.external) {
1166
+ keypair.privateKey.paired({
1167
+ cert: entrada.cert, vault: entrada.iss, proxy: proxy || DEFAULT_PROXY,
1168
+ deviceId: await deviceIdDe(entrada.publickey),
1169
+ name: entrada.acta?.name || entrada.address, profileId: entrada.acta?.profileId || null
1170
+ }).then(
1171
+ () => emitVault({ phase: 'native-account', ok: true }),
1172
+ (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 }) }
1173
+ )
1174
+ }
1150
1175
  return { id: pid, name: entrada.user, address: entrada.address, user: entrada.user, sid: entrada.sid, volatile: !remember }
1151
1176
  } catch (e) {
1152
1177
  // NADA DE MEDIAS CUENTAS: si algo falla, no se queda una identidad a medio adoptar en
@@ -78,6 +78,21 @@ 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).
87
+ */
88
+ async importPair ({ sign, enc }) {
89
+ const k = await bridge.call('import', {
90
+ sign: sign.privateJwk, enc: enc.privateJwk, publickey: sign.publickey, encPub: enc.publickey
91
+ })
92
+ if (!k?.kid) throw Object.assign(new Error('the phone did not keep the key'), { code: 'native-no-key' })
93
+ await keyStore.set(sign.name, { external: k.kid, kind: 'sign', publicJwk: JSON.parse(sign.publickey) })
94
+ await keyStore.set(enc.name, { external: k.kid, kind: 'enc', publicJwk: JSON.parse(enc.publickey) })
95
+ },
81
96
  async create (kind, name) {
82
97
  // Las dos mitades del perfil van bajo la MISMA llave del chip: si la otra ya nació
83
98
  // 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