@dotrino/identity 0.117.1 → 0.118.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.117.1",
3
+ "version": "0.118.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/acta.js CHANGED
@@ -105,7 +105,7 @@ const conCampoSellador = (acta) => Number(acta?.v) < V_SIN_CAMPO_SELLADOR
105
105
  * el rol de master, que no se delega. Así un dispositivo con `admin` robado hace daño
106
106
  * acotado y **reversible** (se le revoca), en vez de poder dejarte fuera de tu cuenta.
107
107
  */
108
- export const CAPS = Object.freeze(['sign', 'store', 'read', 'admin', 'approve', 'passwords', 'passkeys', 'sealer', 'unattended', 'replica'])
108
+ export const CAPS = Object.freeze(['sign', 'store', 'read', 'admin', 'approve', 'passwords', 'sealer', 'unattended', 'replica'])
109
109
 
110
110
  /** Capacidades de un DISPOSITIVO (sin CN): acceso a todo lo del usuario. */
111
111
  /**
@@ -119,14 +119,12 @@ export const CAPS = Object.freeze(['sign', 'store', 'read', 'admin', 'approve',
119
119
  * pedirle algo a la bóveda es exactamente lo que decide el acta — tener dos registros
120
120
  * de lo mismo obliga a acordarse de los dos al quitar un aparato.
121
121
  *
122
- * `passkeys` es ABRIR LA PRIVADA DE UNA PASSKEY. Una passkey no se parece a una
123
- * contraseña: es una llave privada que firma el reto de un sitio, y si se copia sirve
124
- * **hasta que la borres en cada sitio** donde la registraste — no hay nada que «cambiar».
125
- * Por eso su envoltura solo se le hace a quien tenga `passwords` **y** esto
126
- * (`dotrino-passmanager/docs/sealed-passwords.md` §2.8).
127
- *
128
- * Sin `passwords` no significa nada: la passkey vive en una entrada de contraseñas. Y una
129
- * SESIÓN no lo lleva nunca, como `passwords` (`session.js`).
122
+ * Y `passwords` cubre TAMBIÉN LAS PASSKEYS (dueño, 2026-10-16: «realmente deberían ser uno
123
+ * solo»). Hasta la 0.117 había un `passkeys` aparte para abrir la privada de una passkey;
124
+ * se fusionó en este. Un acta vieja que todavía lo nombre sigue valiendo: un permiso
125
+ * desconocido se ignora (ver `checkShape`), y al siguiente cambio del acta desaparece.
126
+ * Lo que distingue una passkey es la POLÍTICA de la bóveda, no un permiso: añadirla es
127
+ * directo, reemplazarla o borrarla pide aprobación (`@dotrino/passmanager`).
130
128
  *
131
129
  * `replica` es REPARTIR, NO DECIDIR. Un replicador no tiene maestra: guarda el acta y los
132
130
  * sobres —que ya vienen sellados a su destinatario, así que tampoco puede abrirlos— y los
@@ -151,7 +149,7 @@ export const CAPS = Object.freeze(['sign', 'store', 'read', 'admin', 'approve',
151
149
  * de la cuenta, se ve en la pantalla de permisos como los demás, y se quita quitándolo —
152
150
  * sin acordarse de un segundo registro escondido.
153
151
  */
154
- export const DEVICE_CAPS = Object.freeze(['sign', 'store', 'read', 'admin', 'approve', 'passwords', 'passkeys', 'sealer', 'unattended', 'replica'])
152
+ export const DEVICE_CAPS = Object.freeze(['sign', 'store', 'read', 'admin', 'approve', 'passwords', 'sealer', 'unattended', 'replica'])
155
153
 
156
154
  /**
157
155
  * Lo que recibe un dispositivo recién emparejado. `admin` **no está**: no se
package/vault/session.js CHANGED
@@ -57,7 +57,7 @@ export const SESSION_SCOPES = Object.freeze(['id:whoami', 'vault:store'])
57
57
  * blanca a propósito: si mañana alguien añade un alcance a `SESSION_SCOPES` sin pensarlo,
58
58
  * esto sigue cortando lo que no puede pasar.
59
59
  */
60
- export const SESSION_FORBIDDEN = Object.freeze(['secrets', 'admin', 'approve', 'sealer', 'passwords', 'passkeys', 'unattended', 'replica'])
60
+ export const SESSION_FORBIDDEN = Object.freeze(['secrets', 'admin', 'approve', 'sealer', 'passwords', 'unattended', 'replica'])
61
61
 
62
62
  /** Qué capacidad del acta hace falta para conceder cada alcance de sesión. */
63
63
  const SCOPE_NEEDS = Object.freeze({ 'id:whoami': null, 'vault:store': 'store' })
@@ -1,3 +1,3 @@
1
- Copia vendorizada de @dotrino/passmanager@0.17.0 (dotrino-passmanager/lib/src/{crypto,fields,generate,import,index,match,model,pubkey,sealed/convert,sealed/device,sealed/entry,sealed/index,sealed/keys,sealed/recovery,sealed/store,session-cache,totp,transport/protocol,transport/proxy,transport/sealed,vault/approval,vault/errors,vault/guard,vault/interface,vault/local,vault/remote,vault/responder,vault/sealed-local,vault/sealed-responder,vault/sealed,webauthn}.js).
1
+ Copia vendorizada de @dotrino/passmanager@0.18.1 (dotrino-passmanager/lib/src/{crypto,fields,generate,import,index,match,model,pubkey,sealed/convert,sealed/device,sealed/entry,sealed/index,sealed/keys,sealed/recovery,sealed/store,session-cache,totp,transport/protocol,transport/proxy,transport/sealed,vault/approval,vault/errors,vault/guard,vault/interface,vault/local,vault/remote,vault/responder,vault/sealed-local,vault/sealed-responder,vault/sealed,webauthn}.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
  El sobre de las contraseñas, el mismo que usan el binario y la extensión. Importa @dotrino/identity/content.
@@ -78,7 +78,7 @@ export async function sealEntry ({ plain, main, passkeys = null, keys }) {
78
78
  if (e.webauthn.privateKey) {
79
79
  if (!passkeys?.cek || typeof passkeys.gen !== 'number') {
80
80
  throw Object.assign(
81
- new Error('this entry carries a passkey: it needs its own generation, wrapped only to devices with `passkeys`'),
81
+ new Error('this entry carries a passkey: it needs its own generation'),
82
82
  { code: 'passkeys-generation-missing' })
83
83
  }
84
84
  fields[PASSKEY_FIELD] = await sealV(passkeys.cek, passkeys.gen, e.webauthn.privateKey)
@@ -38,8 +38,9 @@ export class SealedStore {
38
38
  * @param {object} store `{ get(k), set(k, v) }`
39
39
  * @param {object} opts
40
40
  * `recipients(kind)` → `[{ pub, encPub }]` quién DEBE tener envoltura. `kind` es
41
- * `'main'` (los aparatos con `passwords`) o `'passkeys'` (los que además tienen
42
- * `passkeys`). Lo pone quien conoce el acta: esta pieza no la lee.
41
+ * `'main'` o `'passkeys'` (la generación aparte de la privada de una passkey). Desde
42
+ * 2026-10-16 las dos son los aparatos con `passwords`: el permiso `passkeys` se fusionó
43
+ * en ese. Lo pone quien conoce el acta: esta pieza no la lee.
43
44
  * `verifyAuthor({ body, author })` → ¿la firmó quien dice, y puede escribir? Lo
44
45
  * contesta quien tiene el acta. **No es opcional**: sin ella, cualquiera que llegue
45
46
  * al almacén podría dejar una entrada a nombre de otro, y la bóveda no puede mirar
@@ -97,13 +98,12 @@ export class SealedStore {
97
98
  'the wraps must include the recovery one (#recovery), or this entry could never be handed out again')
98
99
  }
99
100
  const deben = (await this.recipients(kind)).filter((m) => m?.encPub).map((m) => m.pub)
100
- // NADIE A QUIEN ENVOLVÉRSELA NO ES «CERO DESTINATARIOS», ES UN ERROR. Guardar una
101
- // passkey que ningún aparato puede abrir la deja viva solo para la frase del perfil: no
102
- // se pierde, pero no sirve, y el usuario se entera el día que la necesita. Se dice
103
- // ahora, con un código que explica qué falta.
104
- if (kind === 'passkeys' && !deben.length) {
101
+ // Una passkey no se guarda para NADIE: sin aparatos que puedan abrirla y sin copia de
102
+ // recuperación, se perdería entera. Con la copia basta: quien reciba el permiso después
103
+ // la recibe al re-repartir (`rewrapAll`).
104
+ if (kind === 'passkeys' && !deben.length && !wraps[RECOVERY]) {
105
105
  throw new SealedError('no-passkeys-device',
106
- 'no device in the account record can open passkeys: grant `passkeys` to one (`dotrino-vault caps <ID> +passkeys`) before saving it')
106
+ 'nobody could ever open this passkey: no device has `passwords` and the account has no recovery copy')
107
107
  }
108
108
  const hay = Object.keys(wraps).filter((k) => k !== RECOVERY)
109
109
  if (!same(deben, hay)) {
@@ -127,7 +127,7 @@ export class SealedStore {
127
127
  // reparte una llave que nadie va a usar.
128
128
  if (llevaPasskey && !tienePasskeys) {
129
129
  throw new SealedError('passkeys-generation-missing',
130
- 'this entry carries a passkey: it needs its own wraps, only for devices with `passkeys`')
130
+ 'this entry carries a passkey: it needs its own wraps (its own generation)')
131
131
  }
132
132
  if (!llevaPasskey && tienePasskeys) {
133
133
  throw new SealedError('passkeys-generation-unused', 'there is no passkey in this entry to wrap')
@@ -225,7 +225,7 @@ export class SealedStore {
225
225
  if (passkeys) await this.#checkWraps('passkeys', passkeys.wraps)
226
226
  if (fields[PASSKEY_FIELD] && !passkeys) {
227
227
  throw new SealedError('passkeys-generation-missing',
228
- 'that patch carries a passkey: it needs its own wraps, only for devices with `passkeys`')
228
+ 'that patch carries a passkey: it needs its own wraps (its own generation)')
229
229
  }
230
230
 
231
231
  const s = await this.#state()
@@ -343,6 +343,12 @@ export class SealedStore {
343
343
  return s.entries.find((e) => e.id === id)?.priv || []
344
344
  }
345
345
 
346
+ /** ¿Esa entrada lleva una passkey? La bóveda lo sabe sin abrir nada: por su huella `wa`. */
347
+ async hasPasskey (id) {
348
+ const s = await this.#state()
349
+ return !!s.entries.find((e) => e.id === id)?.wa
350
+ }
351
+
346
352
  async remove (id) {
347
353
  const s = await this.#state()
348
354
  const antes = s.entries.length
@@ -119,8 +119,25 @@ export class SealedResponder {
119
119
  }
120
120
  }
121
121
 
122
+ /**
123
+ * ¿CAMBIA O QUITA UNA PASSKEY QUE YA ESTÁ? Añadir es directo y no pregunta; reemplazarla o
124
+ * borrarla, sí — como las variables de `dotrino-env` (dueño, 2026-10-16). Una passkey
125
+ * borrada o pisada deja a alguien fuera de un sitio, y no hay forma de deshacerlo.
126
+ *
127
+ * Subir el contador al entrar NO es reemplazar: cambia la vista, no la llave.
128
+ */
129
+ async replacesPasskey (op, payload) {
130
+ if (op === 'pm2.remove') return this.store.hasPasskey(payload?.id)
131
+ if (op === 'pm2.put') return this.store.hasPasskey(payload?.entry?.id)
132
+ if (op === 'pm2.patch') {
133
+ return !!payload?.fields?.[PASSKEY_FIELD] || (payload?.drop || []).includes(PASSKEY_FIELD)
134
+ }
135
+ return false
136
+ }
137
+
122
138
  async #needsApproval (op, payload, pubkey) {
123
- return await this.needsApproval(pubkey) && await this.wantsPrivate(op, payload)
139
+ if (!await this.needsApproval(pubkey)) return false
140
+ return await this.wantsPrivate(op, payload) || await this.replacesPasskey(op, payload)
124
141
  }
125
142
 
126
143
  async #run (op, payload = {}, pubkey) {
@@ -262,7 +262,11 @@ export class SealedVault {
262
262
  fields: actual.fieldKeys.some((k) => !FIXED.includes(k)),
263
263
  webauthn: actual.hasWebauthn
264
264
  },
265
- ...(actual.webauthn ? { webauthn: actual.webauthn } : {})
265
+ // El contador de la passkey sube en cada firma y vive en la vista: cambiarlo no toca
266
+ // la llave, así que no es reemplazarla (no pide aprobación).
267
+ ...(actual.webauthn
268
+ ? { webauthn: Number.isInteger(changes.signCount) ? { ...actual.webauthn, signCount: changes.signCount } : actual.webauthn }
269
+ : {})
266
270
  }
267
271
  const idx = sites ? await Promise.all(sites.map((s) => siteIndex(keys.kidx, s))) : null
268
272
  const parche = await buildSealedPatch({
@@ -1,4 +1,4 @@
1
- Copia vendorizada de @dotrino/proxy-client@0.35.0 (dotrino-proxy-client/src/{binpack,canonical,client,encpub,index,parts,sealing,signature,stats-log,stats,webrtc}.js).
1
+ Copia vendorizada de @dotrino/proxy-client@0.36.1 (dotrino-proxy-client/src/{binpack,canonical,client,encpub,index,parts,sealing,signature,stats-log,stats,webrtc}.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.
@@ -24,6 +24,13 @@ import { pack, unpack, hasBinary, encodeFrame, decodeFrame } from './binpack.js'
24
24
  * proxio ya tiene atada a esta conexión desde `identify`. Nada del usuario viaja aquí.
25
25
  */
26
26
  const HELLO_TAG = '__cc_hello__'
27
+ /**
28
+ * Lo que este cliente entiende, y lo dice en el saludo (0.36.0). Hoy una cosa: `binary`, las
29
+ * tramas con bytes (binpack.js). Quien saluda sin decirlo es anterior y no las entiende, así
30
+ * que no se le mandan: se lanza `peer-no-binary` y la app lo enseña. La idea del dueño es que
31
+ * todo pase a binario con el tiempo, y esta lista es por donde se sabrá quién ya puede.
32
+ */
33
+ const SPEAKS = ['binary']
27
34
 
28
35
  function errorCon (mensaje, code) {
29
36
  const e = /** @type {Error & { code: string }} */ (new Error(mensaje))
@@ -136,6 +143,10 @@ export class WebSocketProxyClient {
136
143
  * son de nadie.
137
144
  */
138
145
  this._tokenPubkeys = new Map()
146
+ // Lo que dijo entender cada token en su saludo, y cada identidad (para lo que va por
147
+ // pubkey). Sin saludo no se sabe, y entonces no se frena nada.
148
+ this._tokenSpeaks = new Map()
149
+ this._pubkeySpeaks = new Map()
139
150
  this._helloSent = new Set()
140
151
 
141
152
  /** Mi propia identidad en el cable, la que `identify` dejó atada a este token. */
@@ -518,8 +529,9 @@ export class WebSocketProxyClient {
518
529
  if (m.len > queueMax) {
519
530
  throw errorCon(`sendSealed: ${m.len} ${m.bin ? 'bytes' : 'chars'} is more than can wait in the proxy (${queueMax}); send it by token while both ends are connected`, 'too-big-for-queue')
520
531
  }
521
- // Por pubkey todo va por el proxio: si no admite bytes, se dice antes de mandar nada.
522
- if (m.bin) this._needBinaryProxy()
532
+ // Por pubkey todo va por el proxio: si no admite bytes, se dice antes de mandar nada. Y
533
+ // si alguno de los destinatarios dijo que no los entiende, tampoco.
534
+ if (m.bin) { this._needBinaryProxy(); this._needPeerBinary(list) }
523
535
  const partSize = m.bin ? PROXY_PART_BYTES : PROXY_PART_CHARS
524
536
  const pieces = m.len <= partSize ? [m.bin ? m.whole : payload]
525
537
  : m.bin ? splitBinParts(m.whole, partSize) : splitParts(m.whole, partSize)
@@ -579,6 +591,9 @@ export class WebSocketProxyClient {
579
591
  const size = m.bin ? (direct ? DIRECT_PART_BYTES : PROXY_PART_BYTES) : (direct ? DIRECT_PART_CHARS : PROXY_PART_CHARS)
580
592
  const relayMax = m.bin ? RELAY_MAX_BYTES : RELAY_MAX_CHARS
581
593
  const queueMax = m.bin ? QUEUE_MAX_BYTES : QUEUE_MAX_CHARS
594
+ // Por token decide lo que dijo ESA conexión: lo recordado de la identidad puede ser de una
595
+ // versión que ya se actualizó.
596
+ if (m.bin) this._needPeerBinary(tokens)
582
597
  if (m.bin && !direct) this._needBinaryProxy()
583
598
  const sealPiece = await this._sealerFor(m.bin, peerEncPub, peerPubkey)
584
599
  if (m.len <= size) {
@@ -718,7 +733,8 @@ export class WebSocketProxyClient {
718
733
  * Un token es una dirección del proxio y no dice de quién es, así que sin esto no hay
719
734
  * a quién sellarle: una sala de desconocidos se queda muda o —lo que pasaba— hablando
720
735
  * en claro. El saludo lleva SOLO mi llave pública, la misma que el proxio ya tiene
721
- * atada a esta conexión desde `identify`; no hay nada del usuario dentro y por eso no
736
+ * atada a esta conexión desde `identify`, y lo que este cliente entiende (`SPEAKS`, que es
737
+ * lo mismo para todos los de esta versión); no hay nada del usuario dentro y por eso no
722
738
  * necesita sobre.
723
739
  *
724
740
  * Quien lo recibe contesta el suyo una vez, así que basta con que salude UNA de las
@@ -737,7 +753,7 @@ export class WebSocketProxyClient {
737
753
  for (const t of tokens) {
738
754
  if (!t || t === this.token) continue
739
755
  this._helloSent.add(t)
740
- this._proxySendOne(t, { t: HELLO_TAG, publickey: this.myPublickey })
756
+ this._proxySendOne(t, { t: HELLO_TAG, publickey: this.myPublickey, speaks: SPEAKS })
741
757
  }
742
758
  }
743
759
 
@@ -748,9 +764,34 @@ export class WebSocketProxyClient {
748
764
 
749
765
  /** Olvidar un token (se fue, o se quiere volver a preguntar). Sin argumento, todos. */
750
766
  forgetToken (token) {
751
- if (token == null) { this._tokenPubkeys.clear(); this._helloSent.clear(); return }
767
+ if (token == null) { this._tokenPubkeys.clear(); this._helloSent.clear(); this._tokenSpeaks.clear(); return }
752
768
  this._tokenPubkeys.delete(token)
753
769
  this._helloSent.delete(token)
770
+ this._tokenSpeaks.delete(token)
771
+ }
772
+
773
+ /**
774
+ * ¿Entiende esto la otra punta? `true` o `false` si lo dijo en su saludo, y `null` si
775
+ * todavía no ha saludado (no se sabe). Acepta un token o una pubkey.
776
+ * @param {string} who
777
+ * @param {string} [feature]
778
+ * @returns {boolean|null}
779
+ */
780
+ speaks (who, feature = 'binary') {
781
+ let set = this._tokenSpeaks.get(who) || this._pubkeySpeaks.get(who)
782
+ if (!set) {
783
+ for (const [pk, s] of this._pubkeySpeaks) if (samePubkey(pk, who)) { set = s; break }
784
+ }
785
+ return set ? set.has(feature) : null
786
+ }
787
+
788
+ /** Antes de mandar bytes: si alguno dijo que no los entiende, no sale nada y se dice. */
789
+ _needPeerBinary (targets) {
790
+ for (const t of targets) {
791
+ if (t && this.speaks(t, 'binary') === false) {
792
+ throw errorCon('the other side does not take binary messages (it needs @dotrino/proxy-client >= 0.36.0)', 'peer-no-binary')
793
+ }
794
+ }
754
795
  }
755
796
 
756
797
  _onHello (from, msg) {
@@ -764,6 +805,9 @@ export class WebSocketProxyClient {
764
805
  return
765
806
  }
766
807
  this._tokenPubkeys.set(from, msg.publickey)
808
+ const speaks = new Set(Array.isArray(msg.speaks) ? msg.speaks.filter((x) => typeof x === 'string').slice(0, 32) : [])
809
+ this._tokenSpeaks.set(from, speaks)
810
+ this._pubkeySpeaks.set(msg.publickey, speaks)
767
811
  // Contestar UNA vez: el saludo queda simétrico sin rebotar para siempre.
768
812
  if (!this._helloSent.has(from) && this.myPublickey) this.helloTo(from)
769
813
  this._emit('peer_identity', from, msg.publickey)
@@ -1,3 +1,3 @@
1
- Copia vendorizada de @dotrino/roadmap@0.4.17 (dotrino-roadmap/src/{index,manifests}.js).
1
+ Copia vendorizada de @dotrino/roadmap@0.4.20 (dotrino-roadmap/src/{index,manifests}.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
  El registro de versiones rotas que viaja con cada versión. Importa @dotrino/compat/ranges.
@@ -8,7 +8,7 @@
8
8
  export const MANIFESTS = Object.freeze({
9
9
  "dotrino": {
10
10
  "manifest": "dotrino",
11
- "seq": 27,
11
+ "seq": 29,
12
12
  "issued": "2026-10-15",
13
13
  "note": "Registro comun de compatibilidad del ecosistema. Se edita a mano en este repo y se consulta con @dotrino/roadmap. Rangos: ver @dotrino/compat/ranges (x.y.z, >= > <= <, «a - b», «*», o una lista que es un O).",
14
14
  "products": {
@@ -31,7 +31,7 @@ export const MANIFESTS = Object.freeze({
31
31
  "identity": {
32
32
  "repo": "imdotrino/dotrino-identity",
33
33
  "npm": "@dotrino/identity",
34
- "current": "0.116.0",
34
+ "current": "0.117.2",
35
35
  "protocol": 1,
36
36
  "requires": {}
37
37
  },
@@ -47,13 +47,13 @@ export const MANIFESTS = Object.freeze({
47
47
  },
48
48
  "messenger": {
49
49
  "repo": "imdotrino/dotrino-messenger",
50
- "current": "0.7.0",
50
+ "current": "0.8.1",
51
51
  "protocol": 2,
52
52
  "requires": {
53
- "identity": ">=0.108.0",
54
- "proxy-client": ">=0.34.0",
53
+ "identity": ">=0.117.0",
54
+ "proxy-client": ">=0.36.1",
55
55
  "store": ">=0.12.3",
56
- "proxy": "1.5.2+"
56
+ "proxy": "1.6.1+"
57
57
  }
58
58
  },
59
59
  "messenger-android": {
@@ -107,7 +107,7 @@ export const MANIFESTS = Object.freeze({
107
107
  },
108
108
  "proxy": {
109
109
  "repo": "imdotrino/dotrino-proxy",
110
- "current": "1.5.2",
110
+ "current": "1.6.1",
111
111
  "protocol": 2,
112
112
  "requires": {
113
113
  "vault": ">=0.86.0",
@@ -119,7 +119,7 @@ export const MANIFESTS = Object.freeze({
119
119
  "proxy-client": {
120
120
  "repo": "imdotrino/dotrino-proxy-client",
121
121
  "npm": "@dotrino/proxy-client",
122
- "current": "0.34.0",
122
+ "current": "0.36.1",
123
123
  "protocol": 2,
124
124
  "requires": {
125
125
  "identity": ">=0.53.0",
@@ -129,25 +129,25 @@ export const MANIFESTS = Object.freeze({
129
129
  "remote-agent": {
130
130
  "repo": "imdotrino/dotrino-remote-agent",
131
131
  "npm": "@dotrino/remote-agent",
132
- "current": "0.19.1",
132
+ "current": "0.20.1",
133
133
  "protocol": 1,
134
134
  "requires": {
135
- "identity": ">=0.115.0",
135
+ "identity": ">=0.117.2",
136
136
  "vault": ">=0.85.0",
137
- "proxy-client": ">=0.30.0",
137
+ "proxy-client": ">=0.36.1",
138
138
  "vaultd": ">=0.154.0"
139
139
  }
140
140
  },
141
141
  "terminal-agent": {
142
142
  "repo": "imdotrino/dotrino-terminal",
143
143
  "npm": "@dotrino/terminal-agent",
144
- "current": "0.47.4",
144
+ "current": "0.48.0",
145
145
  "protocol": 2,
146
146
  "requires": {
147
- "remote-agent": ">=0.18.3",
148
- "identity": ">=0.116.0",
149
- "vault": ">=0.86.0",
150
- "proxy-client": ">=0.31.1",
147
+ "remote-agent": ">=0.20.1",
148
+ "identity": ">=0.117.2",
149
+ "vault": ">=0.87.0",
150
+ "proxy-client": ">=0.36.1",
151
151
  "vaultd": ">=0.142.0"
152
152
  }
153
153
  },
@@ -250,12 +250,12 @@ export const MANIFESTS = Object.freeze({
250
250
  "vault": {
251
251
  "repo": "imdotrino/dotrino-vault",
252
252
  "npm": "@dotrino/vault",
253
- "current": "0.86.0",
253
+ "current": "0.87.0",
254
254
  "protocol": 1,
255
255
  "requires": {
256
256
  "vaultd": ">=0.105.0",
257
- "proxy-client": "0.24.0+",
258
- "identity": ">=0.116.0",
257
+ "proxy-client": ">=0.36.1",
258
+ "identity": ">=0.117.2",
259
259
  "opaque": ">=0.1.0",
260
260
  "compat": ">=0.4.0",
261
261
  "roadmap": ">=0.4.13",
@@ -266,11 +266,11 @@ export const MANIFESTS = Object.freeze({
266
266
  "vaultd": {
267
267
  "repo": "imdotrino/dotrino-vault",
268
268
  "npm": "@dotrino/vaultd",
269
- "current": "0.157.0",
269
+ "current": "0.159.0",
270
270
  "protocol": 1,
271
271
  "requires": {
272
- "identity": ">=0.116.0",
273
- "proxy-client": ">=0.30.1",
272
+ "identity": ">=0.117.2",
273
+ "proxy-client": ">=0.36.1",
274
274
  "store": ">=0.11.0",
275
275
  "passmanager": ">=0.17.0",
276
276
  "compat": ">=0.4.0",
@@ -290,11 +290,11 @@ export const MANIFESTS = Object.freeze({
290
290
  "passmanager": {
291
291
  "repo": "imdotrino/dotrino-passmanager",
292
292
  "npm": "@dotrino/passmanager",
293
- "current": "0.17.0",
293
+ "current": "0.18.0",
294
294
  "protocol": 1,
295
295
  "requires": {
296
- "identity": ">=0.61.0",
297
- "proxy-client": ">=0.13.0"
296
+ "identity": ">=0.117.2",
297
+ "proxy-client": ">=0.36.1"
298
298
  }
299
299
  },
300
300
  "passmanager-android": {
@@ -317,12 +317,12 @@ export const MANIFESTS = Object.freeze({
317
317
  },
318
318
  "passmanager-extension": {
319
319
  "repo": "imdotrino/dotrino-passmanager",
320
- "current": "0.49.1",
320
+ "current": "0.51.1",
321
321
  "protocol": 1,
322
322
  "requires": {
323
- "identity": ">=0.116.0",
324
- "proxy-client": ">=0.32.2",
325
- "vault": ">=0.86.0",
323
+ "identity": ">=0.117.2",
324
+ "proxy-client": ">=0.36.1",
325
+ "vault": ">=0.87.0",
326
326
  "proxy": "1.1.0+"
327
327
  }
328
328
  },
@@ -349,6 +349,30 @@ export const MANIFESTS = Object.freeze({
349
349
  }
350
350
  },
351
351
  "broken": [
352
+ {
353
+ "product": "passmanager-extension",
354
+ "versions": "0.50.0",
355
+ "why": "la página que pide una passkey decía ella misma de qué sitio era (rpId y origen salían del mensaje, no del remitente): un sitio cualquiera podía pedir la firma de la passkey de otro sitio guardada en Dotrino",
356
+ "fix": "sube a la extensión 0.51.0 (se actualiza sola desde pass.dotrino.com/app)"
357
+ },
358
+ {
359
+ "product": "passmanager-extension",
360
+ "versions": "0.49.1",
361
+ "why": "la página que pide una passkey decía ella misma de qué sitio era (rpId y origen salían del mensaje, no del remitente): un sitio cualquiera podía pedir la firma de la passkey de otro sitio guardada en Dotrino",
362
+ "fix": "sube a la extensión 0.51.0 (se actualiza sola desde pass.dotrino.com/app)"
363
+ },
364
+ {
365
+ "product": "passmanager-extension",
366
+ "versions": "0.49.0",
367
+ "why": "la página que pide una passkey decía ella misma de qué sitio era (rpId y origen salían del mensaje, no del remitente): un sitio cualquiera podía pedir la firma de la passkey de otro sitio guardada en Dotrino",
368
+ "fix": "sube a la extensión 0.51.0 (se actualiza sola desde pass.dotrino.com/app)"
369
+ },
370
+ {
371
+ "product": "passmanager-extension",
372
+ "versions": "0.48.4",
373
+ "why": "la página que pide una passkey decía ella misma de qué sitio era (rpId y origen salían del mensaje, no del remitente): un sitio cualquiera podía pedir la firma de la passkey de otro sitio guardada en Dotrino",
374
+ "fix": "sube a la extensión 0.51.0 (se actualiza sola desde pass.dotrino.com/app)"
375
+ },
352
376
  {
353
377
  "product": "terminal-agent",
354
378
  "versions": "0.46.4",
@@ -796,6 +820,18 @@ export const MANIFESTS = Object.freeze({
796
820
  "versions": "1.9.0",
797
821
  "why": "no tiene UI propia: sus tres superficies son iframes a messenger.dotrino.com con ?embed=, y el soporte de ese modo (puente de identidad, espejo de conversaciones, cola de salida, aviso de DM) se quitó del messenger después de la 0.1.0; la PWA servida siempre es la última, así que el panel se inventa una cuenta en cada sitio y no recibe nada",
798
822
  "fix": "no hay versión que funcione: hay que elegir el camino del README de dotrino-messenger-extension (extensión autónoma con los pilares, o un modo embed del ecosistema)"
823
+ },
824
+ {
825
+ "product": "proxy",
826
+ "versions": "1.6.0",
827
+ "why": "pasa a base64 los mensajes binarios para cruzar la malla, y una parte de 768 KB así pasa de la trama de 1 MB del otro nodo: el enlace se cae en cada reconexión al reenviarla y todo lo que va detrás entre los dos nodos se queda atascado",
828
+ "fix": "websocket-proxy 1.6.1"
829
+ },
830
+ {
831
+ "product": "proxy-client",
832
+ "versions": "0.36.0",
833
+ "why": "por token frena los bytes por lo recordado de la identidad (que puede ser de antes de que el otro se actualizara), y no solo por lo que dijo esa conexión en su saludo",
834
+ "fix": "@dotrino/proxy-client 0.36.1"
799
835
  }
800
836
  ]
801
837
  }
@@ -1,4 +1,4 @@
1
- Copia vendorizada de @dotrino/vault@0.86.1 (dotrino-vault/lib/src/{admin,b64,browser,core/approvals,core/blocked,core/browserDisk,core/commKey,core/disk,core/libVersion,core/machines,core/notices,core/notifiable,core/passwords,core/sealKey,core/sealer,core/sealers,core/secretsStore,core/sha256,core/store,core/subacta,core/threadStore,core/transport,core/vault,enroll,index,loginClient,passwordLogins,procContext,protocol,revocation,sealed}.js).
1
+ Copia vendorizada de @dotrino/vault@0.88.0 (dotrino-vault/lib/src/{admin,b64,browser,core/approvals,core/blocked,core/browserDisk,core/commKey,core/disk,core/libVersion,core/machines,core/notices,core/notifiable,core/passwords,core/sealKey,core/sealer,core/sealers,core/secretsStore,core/sha256,core/store,core/subacta,core/threadStore,core/transport,core/vault,enroll,index,loginClient,passwordLogins,procContext,protocol,revocation,sealed}.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
  browser.js es la bóveda de esta pestaña: el MISMO núcleo que el binario (core/), con
4
4
  el disco en IndexedDB. Importa @dotrino/{compat,roadmap,passmanager,store/core} y
@@ -5,4 +5,4 @@
5
5
  * si no coincide con la del paquete: una versión anunciada que no es la que corre es peor
6
6
  * que no anunciar ninguna.
7
7
  */
8
- export const LIB_VERSION = '0.86.0'
8
+ export const LIB_VERSION = '0.88.0'
@@ -23,7 +23,6 @@ import { SealedStore, SealedResponder } from '@dotrino/passmanager'
23
23
  import { verifyDeviceSig } from '@dotrino/identity/capabilities'
24
24
 
25
25
  export const PASSWORDS_CAP = 'passwords'
26
- export const PASSKEYS_CAP = 'passkeys'
27
26
 
28
27
  /**
29
28
  * @param {object} opts
@@ -311,14 +311,14 @@ export async function startVault ({ platform, proxyUrl, log = console.log, onEnr
311
311
  * eso se filtran explícitamente aquí, que es donde se construye la lista, y no en quien la
312
312
  * consume (`sealed-passwords.md` §2.1).
313
313
  *
314
- * `main` son los aparatos con `contraseñas`; `passkeys`, los que además pueden abrir la
315
- * privada de una passkey (§2.8). Un miembro sin llave de cifrado no puede recibir nada: se
314
+ * `main` y `passkeys` (la generación aparte de la privada de una passkey) son los MISMOS:
315
+ * los aparatos con `contraseñas`. Hubo un permiso `passkeys` aparte y se fusionó en ese
316
+ * (dueño, 2026-10-16: «realmente deberían ser uno solo»); `kind` se queda por el formato. Un miembro sin llave de cifrado no puede recibir nada: se
316
317
  * queda fuera y se ve en `incompleteMembers`.
317
318
  */
318
319
  const noEsLaBoveda = (pub) => pub !== master && pub !== commKey.pub()
319
- const passwordRecipients = (kind = 'main') => (actaCache?.members || [])
320
+ const passwordRecipients = (_kind = 'main') => (actaCache?.members || [])
320
321
  .filter((m) => m.encPub && noEsLaBoveda(m.pub) && Acta.memberCan(actaCache, m.pub, 'passwords'))
321
- .filter((m) => kind !== 'passkeys' || Acta.memberCan(actaCache, m.pub, 'passkeys'))
322
322
  .map((m) => ({ pub: m.pub, encPub: m.encPub }))
323
323
 
324
324
  /**