@dotrino/identity 0.117.2 → 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.2",
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,3 +1,3 @@
1
- Copia vendorizada de @dotrino/roadmap@0.4.18 (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": 28,
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.117.1",
34
+ "current": "0.117.2",
35
35
  "protocol": 1,
36
36
  "requires": {}
37
37
  },
@@ -47,11 +47,11 @@ export const MANIFESTS = Object.freeze({
47
47
  },
48
48
  "messenger": {
49
49
  "repo": "imdotrino/dotrino-messenger",
50
- "current": "0.8.0",
50
+ "current": "0.8.1",
51
51
  "protocol": 2,
52
52
  "requires": {
53
53
  "identity": ">=0.117.0",
54
- "proxy-client": ">=0.35.0",
54
+ "proxy-client": ">=0.36.1",
55
55
  "store": ">=0.12.3",
56
56
  "proxy": "1.6.1+"
57
57
  }
@@ -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.35.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",
@@ -802,6 +826,12 @@ export const MANIFESTS = Object.freeze({
802
826
  "versions": "1.6.0",
803
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",
804
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"
805
835
  }
806
836
  ]
807
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.1'
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
  /**