@dotrino/identity 0.114.1 → 0.116.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.
Files changed (81) hide show
  1. package/package.json +7 -1
  2. package/src/index.js +9 -1
  3. package/src/node.js +4 -180
  4. package/vault/acta.js +5 -1
  5. package/vault/core.js +95 -15
  6. package/vault/facade.js +200 -0
  7. package/vault/index.html +10 -0
  8. package/vault/machine.js +95 -0
  9. package/vault/remote.js +54 -9
  10. package/vault/vault.js +29 -58
  11. package/vault/vendor/compat/VERSION.txt +3 -0
  12. package/vault/vendor/compat/advisory.js +141 -0
  13. package/vault/vendor/compat/index.js +188 -0
  14. package/vault/vendor/compat/ranges.js +93 -0
  15. package/vault/vendor/opaque/VERSION.txt +1 -1
  16. package/vault/vendor/passmanager/VERSION.txt +3 -0
  17. package/vault/vendor/passmanager/crypto.js +167 -0
  18. package/vault/vendor/passmanager/fields.js +183 -0
  19. package/vault/vendor/passmanager/generate.js +74 -0
  20. package/vault/vendor/passmanager/import.js +156 -0
  21. package/vault/vendor/passmanager/index.js +27 -0
  22. package/vault/vendor/passmanager/match.js +137 -0
  23. package/vault/vendor/passmanager/model.js +332 -0
  24. package/vault/vendor/passmanager/pubkey.js +33 -0
  25. package/vault/vendor/passmanager/sealed/convert.js +72 -0
  26. package/vault/vendor/passmanager/sealed/device.js +211 -0
  27. package/vault/vendor/passmanager/sealed/entry.js +209 -0
  28. package/vault/vendor/passmanager/sealed/index.js +20 -0
  29. package/vault/vendor/passmanager/sealed/keys.js +120 -0
  30. package/vault/vendor/passmanager/sealed/recovery.js +136 -0
  31. package/vault/vendor/passmanager/sealed/store.js +508 -0
  32. package/vault/vendor/passmanager/session-cache.js +60 -0
  33. package/vault/vendor/passmanager/totp.js +111 -0
  34. package/vault/vendor/passmanager/transport/protocol.js +83 -0
  35. package/vault/vendor/passmanager/transport/proxy.js +112 -0
  36. package/vault/vendor/passmanager/transport/sealed.js +74 -0
  37. package/vault/vendor/passmanager/vault/approval.js +63 -0
  38. package/vault/vendor/passmanager/vault/errors.js +34 -0
  39. package/vault/vendor/passmanager/vault/guard.js +114 -0
  40. package/vault/vendor/passmanager/vault/interface.js +34 -0
  41. package/vault/vendor/passmanager/vault/local.js +213 -0
  42. package/vault/vendor/passmanager/vault/remote.js +85 -0
  43. package/vault/vendor/passmanager/vault/responder.js +192 -0
  44. package/vault/vendor/passmanager/vault/sealed-local.js +199 -0
  45. package/vault/vendor/passmanager/vault/sealed-responder.js +159 -0
  46. package/vault/vendor/passmanager/vault/sealed.js +362 -0
  47. package/vault/vendor/passmanager/webauthn.js +221 -0
  48. package/vault/vendor/proxy-client/VERSION.txt +1 -1
  49. package/vault/vendor/proxy-client/client.js +36 -0
  50. package/vault/vendor/roadmap/VERSION.txt +3 -0
  51. package/vault/vendor/roadmap/index.js +108 -0
  52. package/vault/vendor/roadmap/manifests.js +797 -0
  53. package/vault/vendor/store/VERSION.txt +3 -0
  54. package/vault/vendor/store/core.js +309 -0
  55. package/vault/vendor/vault/VERSION.txt +4 -1
  56. package/vault/vendor/vault/admin.js +437 -0
  57. package/vault/vendor/vault/browser.js +167 -0
  58. package/vault/vendor/vault/core/approvals.js +204 -0
  59. package/vault/vendor/vault/core/blocked.js +68 -0
  60. package/vault/vendor/vault/core/browserDisk.js +109 -0
  61. package/vault/vendor/vault/core/commKey.js +68 -0
  62. package/vault/vendor/vault/core/disk.js +65 -0
  63. package/vault/vendor/vault/core/libVersion.js +8 -0
  64. package/vault/vendor/vault/core/machines.js +66 -0
  65. package/vault/vendor/vault/core/notices.js +71 -0
  66. package/vault/vendor/vault/core/notifiable.js +73 -0
  67. package/vault/vendor/vault/core/passwords.js +125 -0
  68. package/vault/vendor/vault/core/sealKey.js +77 -0
  69. package/vault/vendor/vault/core/sealer.js +172 -0
  70. package/vault/vendor/vault/core/sealers.js +159 -0
  71. package/vault/vendor/vault/core/secretsStore.js +1326 -0
  72. package/vault/vendor/vault/core/sha256.js +69 -0
  73. package/vault/vendor/vault/core/store.js +68 -0
  74. package/vault/vendor/vault/core/subacta.js +101 -0
  75. package/vault/vendor/vault/core/threadStore.js +273 -0
  76. package/vault/vendor/vault/core/transport.js +270 -0
  77. package/vault/vendor/vault/core/vault.js +4414 -0
  78. package/vault/vendor/vault/enroll.js +8 -4
  79. package/vault/vendor/vault/procContext.js +97 -0
  80. package/vault/vendor/vault/revocation.js +67 -0
  81. package/vault/vendor/vault/sealed.js +84 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/identity",
3
- "version": "0.114.1",
3
+ "version": "0.116.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",
@@ -20,8 +20,14 @@
20
20
  "./avatar": {
21
21
  "import": "./vault/avatar.js"
22
22
  },
23
+ "./facade": {
24
+ "import": "./vault/facade.js"
25
+ },
23
26
  "./vault/core.js": "./vault/core.js",
24
27
  "./vault/remote.js": "./vault/remote.js",
28
+ "./machine": {
29
+ "import": "./vault/machine.js"
30
+ },
25
31
  "./acta": {
26
32
  "import": "./vault/acta.js"
27
33
  },
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
- return this._call('vaultPair', { qr, label, join }, 200000)
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/src/node.js CHANGED
@@ -19,6 +19,7 @@ import fs from 'node:fs'
19
19
  import path from 'node:path'
20
20
  import os from 'node:os'
21
21
  import { createIdentityCore } from '../vault/core.js'
22
+ import { IdentityFacade } from '../vault/facade.js'
22
23
 
23
24
  /**
24
25
  * kv síncrono respaldado por un archivo JSON (estilo localStorage).
@@ -93,21 +94,20 @@ function filePeers (filePath) {
93
94
 
94
95
  const DEFAULT_DIR = path.join(os.homedir(), '.dotrino', 'identity')
95
96
 
96
- export class Identity {
97
+ export class Identity extends IdentityFacade {
97
98
  /**
98
99
  * @param {Object} [options]
99
100
  * @param {string} [options.dir] Directorio de persistencia de esta identidad.
100
101
  * Cada directorio = un usuario distinto. Default: ~/.dotrino/identity
101
102
  */
102
103
  constructor (options = {}) {
104
+ super(null)
103
105
  this._dir = options.dir || DEFAULT_DIR
104
106
  this._atRest = options.atRest || null
105
107
  // CANDADO DE LA MAESTRA: `{ seal(jwkStr), open(blob) }`. Quien lo provee (el daemon
106
108
  // del vault) es el único que tiene la llave que sale de la contraseña. Sin candado,
107
109
  // la maestra se guarda como siempre — un perfil sin contraseña no cambia.
108
110
  this._keyLock = options.keyLock || null
109
- this._core = null
110
- this._listeners = new Map()
111
111
  }
112
112
 
113
113
  /**
@@ -133,186 +133,10 @@ export class Identity {
133
133
  // apuntando a algo que ya no existe. Se borra el enlace y el acta, nada más.
134
134
  removeAccountOnExpulsion: false
135
135
  })
136
- this._core.onSyncStatus((payload) => this._emit('sync', payload))
137
- this._core.onVaultEvent((payload) => this._emit('vault', payload))
136
+ this._wire()
138
137
  return this
139
138
  }
140
139
 
141
- destroy () { this._core = null }
142
-
143
- get me () { return this._core?.me || null }
144
-
145
- // ----- candado de la maestra -----
146
- /** `true` si la maestra está sellada: esta identidad NO puede firmar nada. */
147
- get masterLocked () { return !!this._core?.masterLocked }
148
- /** Sella la maestra que ya existe (se llama al abrir el perfil, con la llave en la mano). */
149
- sealMasterKey () { return this._core?.sealMasterKey?.() }
150
- /** Vuelve a cargar el par tras abrir el candado, sin reabrir la identidad. */
151
- reloadMasterKey () { return this._core?.reloadMasterKey?.() }
152
- /** Ver `core.js`: al abrir, los aparatos muertos salen del acta en una sola. */
153
- pruneExpiredDevices (now) { return this._core?.pruneExpiredDevices?.(now) }
154
-
155
- _h (method, params = {}) {
156
- if (!this._core) throw new Error('Identity not ready — call ready()/connect() first')
157
- return this._core.handlers[method](params)
158
- }
159
-
160
- // ----- API pública (espeja src/index.js) -----
161
-
162
- makeChallenge () { return this._h('makeChallenge') }
163
- signChallenge (nonce) { return this._h('signChallenge', { nonce }) }
164
- verifyResponse ({ nonce, publickey, signature, encryptionPubkey }) {
165
- return this._h('verifyResponse', { nonce, publickey, signature, encryptionPubkey })
166
- }
167
- getPeer (publickey) { return this._h('getPeer', { publickey }) }
168
- setNickname (publickey, nickname) { return this._h('setNickname', { publickey, nickname }) }
169
- setBlocked (publickey, blocked) { return this._h('setBlocked', { publickey, blocked }) }
170
- setRating (publickey, rating, notes) { return this._h('setRating', { publickey, rating, notes }) }
171
- listPeers () { return this._h('listPeers') }
172
- forgetPeer (publickey) { return this._h('forgetPeer', { publickey }) }
173
- addContact ({ publickey, nickname, encryptionPubkey, lastToken, notes } = {}) {
174
- return this._h('addContact', { publickey, nickname, encryptionPubkey, lastToken, notes })
175
- }
176
- updateContact (publickey, patch) { return this._h('updateContact', { publickey, patch }) }
177
- removeContact (publickey) { return this._h('removeContact', { publickey }) }
178
- listContacts () { return this._h('listContacts') }
179
- signData (data) { return this._h('signData', { data }) }
180
- /**
181
- * Prueba firmada con destinatario y vigencia (ver `vault/assertion.js`). Un servicio en
182
- * Node la pide igual que una app: `{ audience, nonce, scopes?, ttlMs? }`.
183
- */
184
- async requestAssertion ({ audience, nonce, scopes, ttlMs } = {}) {
185
- const { assertion } = await this._h('requestAssertion', { audience, nonce, scopes, ttlMs })
186
- return assertion
187
- }
188
- // Delegación de capacidad (sub-clave de dispositivo con scope/exp/revocación)
189
- signDelegation (sub, scope, opts = {}) { return this._h('signDelegation', { sub, scope, ...opts }) }
190
- revokeDelegation (nonce) { return this._h('revokeDelegation', { nonce }) }
191
- /** Quitar el DISPOSITIVO: retira todos sus certificados vigentes, no solo uno. */
192
- revokeDevice (sub) { return this._h('revokeDevice', { sub }) }
193
- listDelegations () { return this._h('listDelegations') }
194
- // Acta de perfil (qué llaves son del perfil y qué puede cada una; ver acta-de-perfil.md)
195
- profileActa () { return this._h('profileActa') }
196
- /** La cadena de selladores, para mandarla con una firma (ver `vault/acta.js`). */
197
- sealerChain () { return this._h('sealerChain') }
198
- profileMembers () { return this._h('profileMembers') }
199
- /**
200
- * Quien SELLA SOBRES (la bóveda) registra aquí cómo estrenar su llave de sellado: se
201
- * llama al sellar cada acta, porque esa llave rota con el acta (§8.9 de
202
- * `dotrino-vault/docs/secretos-sellados.md`). Va por aquí y no por el iframe porque
203
- * este puente es en-proceso: al navegador no se le puede pasar una función, y tampoco
204
- * la necesita — no sella secretos de nadie.
205
- */
206
- /** Abre un sobre sellado A ESTE aparato (envoltura + sobre). Ver `core.js`. */
207
- openSealedValue (wrap, envelope) { return this._h('openSealedValue', { wrap, envelope }) }
208
- /** Re-envuelve para otro miembro una llave que este aparato ya puede abrir. */
209
- rewrapFor (wrap, encPub) { return this._h('rewrapFor', { wrap, encPub }) }
210
- setSealKeyProvider (provider) { return this._h('setSealKeyProvider', { provider }) }
211
- /** Estrena la llave de sellado en un acta nueva, sin tocar a ningún miembro. */
212
- rotateSealKey () { return this._h('rotateSealKey') }
213
- /** Con qué llave se comprueban los sobres que se firmaron con el acta `seq`. */
214
- sealKeyAt (seq) { return this._h('sealKeyAt', { seq }) }
215
- myMembership () { return this._h('myMembership') }
216
- /**
217
- * La CADENA de actas desde `sinceSeq` (para que un dispositivo que estuvo apagado
218
- * compruebe el encadenamiento en vez de tragarse un salto a ciegas).
219
- *
220
- * Vivía en el núcleo pero NO estaba aquí, así que `identity.actaHistory` era
221
- * `undefined` en el daemon: al pedir la lista de dispositivos con un `sinceSeq`
222
- * —que es lo que manda SIEMPRE un navegador ya emparejado— reventaba con
223
- * «is not a function», la bóveda respondía error, y el aparato NUNCA volvía a
224
- * sincronizar su acta. Se quedaba con la copia del día que emparejó.
225
- */
226
- actaHistory (opts) { return this._h('actaHistory', opts || {}) }
227
- isMaster () { return this._h('isMaster') }
228
- admitMember (member) { return this._h('admitMember', member) }
229
- /** Registra la llave de cifrado de un miembro ya admitido (evita re-enrolarlo). */
230
- setMemberEncPub (args) { return this._h('setMemberEncPub', args) }
231
- setCaps (pub, caps) { return this._h('setCaps', { pub, caps }) }
232
- setCapsMany (changes) { return this._h('setCapsMany', { changes }) }
233
- setLabel (pub, label) { return this._h('setLabel', { pub, label }) }
234
- removeMember (pub) { return this._h('removeMember', { pub }) }
235
- handoverMaster (to, member = null) { return this._h('handoverMaster', { to, member }) }
236
- renounceCaps (caps) { return this._h('renounceCaps', { caps }) }
237
- absorbRenounce (record) { return this._h('absorbRenounce', { record }) }
238
- adoptActa (acta) { return this._h('adoptActa', { acta }) }
239
- /** Une ESTE perfil a la cuenta de otro (solo si nació para eso: `createProfile(n, { forVault: true })`). */
240
- joinProfile (acta) { return this._h('joinProfile', { acta }) }
241
- /** Marca este perfil como nacido para ADOPTAR la cuenta de otro (camino A: lo usa la bóveda). */
242
- prepareForAdoption () { return this._h('prepareForAdoption') }
243
- /** MI tarjeta de perfil: lo mínimo que un contacto necesita para cifrarme a todos mis
244
- * dispositivos (perfil, versión y llaves). Sin etiquetas ni permisos. */
245
- profileCard () { return this._h('profileCard') }
246
- /** Guarda la tarjeta de otra persona en su ficha de contacto (verificándola). */
247
- adoptPeerCard (card) { return this._h('adoptPeerCard', { card }) }
248
- /** La clave de contenido del perfil, abierta con la llave de cifrado de este dispositivo. */
249
- contentKey () { return this._h('contentKey') }
250
- /** Cifra con la clave de contenido del perfil (la privada de cifrado no sale del vault). */
251
- sealContent (plaintext) { return this._h('sealContent', { plaintext }) }
252
- /** Abre un sobre de contenido con el llavero del perfil. */
253
- openContent (envelope) { return this._h('openContent', { envelope }) }
254
- /** Rota la clave de contenido (corta el acceso al contenido FUTURO de quien ya no está). */
255
- rotateContentKey () { return this._h('rotateContentKey') }
256
- // Emparejar ESTE dispositivo con el vault del usuario (Fase 1)
257
- enrollDevice (qr, { label = '', join = 'current' } = {}) { return this._h('vaultPair', { qr, label, join }) }
258
- vaultStatus () { return this._h('vaultStatus') }
259
- unpairDevice () { return this._h('vaultUnpair') }
260
- vaultSign (payload) { return this._h('vaultSign', { payload }) }
261
- vaultStore (method, args) { return this._h('vaultStore', { method, args }) }
262
- listVaultDevices () { return this._h('listVaultDevices') }
263
- getVaultCert () { return this._h('getVaultCert') }
264
- vaultApprovals (op, args) { return this._h('vaultApprovals', { op, ...(args || {}) }) }
265
- /** Los pedidos de TODAS las cuentas que aprueban, sin cambiar la activa. */
266
- vaultApprovalsAll (args) { return this._h('vaultApprovalsAll', args || {}) }
267
- onVault (handler) { return this.on('vault', handler) }
268
- // Multi-perfil por dispositivo (crear/cambiar reinicializa con el nuevo perfil activo).
269
- listProfiles () { return this._h('listProfiles') }
270
- currentProfile () { return this._h('currentProfile') }
271
- createProfile (name, { forVault = false } = {}) { return this._h('createProfile', { name, forVault }) }
272
- switchProfile (id) { return this._h('switchProfile', { id }) }
273
- renameProfile (id, name) { return this._h('renameProfile', { id, name }) }
274
- deleteProfile (id) { return this._h('deleteProfile', { id }) }
275
- mergeEndorsements (subject, endorsements, askerPubkey) {
276
- return this._h('mergeEndorsements', { subject, endorsements, askerPubkey })
277
- }
278
- getRatingsForSubject (subject) { return this._h('getRatingsForSubject', { subject }) }
279
- recordQuery (askerPubkey, subject) { return this._h('recordQuery', { askerPubkey, subject }) }
280
- async setMyNickname (nickname) {
281
- const result = await this._h('setMyNickname', { nickname })
282
- return result
283
- }
284
- async updateMe (patch) { return this._h('updateMe', { patch }) }
285
- getMe () { return this._h('getMe') }
286
- publicMe () { return this._h('publicMe') }
287
- /** Cómo fue el último empujón del perfil a la bóveda: `{ ok, at, error }`. */
288
- profilePushState () { return this._h('profilePushState') }
289
- getEncryptionPubkey () { return this._h('getEncryptionPubkey') }
290
- encrypt (recipients, plaintext) { return this._h('encrypt', { recipients, plaintext }) }
291
- decrypt (senderEncryptionPubkey, myToken, envelope) {
292
- return this._h('decrypt', { senderEncryptionPubkey, myToken, envelope })
293
- }
294
- exportIdentity () { return this._h('exportIdentity') }
295
- async importIdentity (blob) { return this._h('importIdentity', blob || {}) }
296
-
297
- // Sync (Google Drive) no disponible headless — los handlers responden acorde.
298
- syncConnect (clientId) { return this._h('syncConnect', { clientId }) }
299
- syncDisconnect () { return this._h('syncDisconnect') }
300
- syncUnlock (passphrase) { return this._h('syncUnlock', { passphrase }) }
301
- syncLock () { return this._h('syncLock') }
302
- syncStatus () { return this._h('syncStatus') }
303
- syncNow () { return this._h('syncNow') }
304
-
305
- onSync (handler) { return this.on('sync', handler) }
306
- on (event, handler) {
307
- if (!this._listeners.has(event)) this._listeners.set(event, new Set())
308
- this._listeners.get(event).add(handler)
309
- return () => this._listeners.get(event)?.delete(handler)
310
- }
311
- _emit (event, payload) {
312
- const set = this._listeners.get(event)
313
- if (!set) return
314
- for (const h of set) { try { h(payload) } catch (e) { console.error(e) } }
315
- }
316
140
  }
317
141
 
318
142
  export default Identity
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
@@ -24,7 +24,7 @@ import * as Content from './content.js'
24
24
  import { syncPeers, stampOf } from './peerSync.js'
25
25
  import { assertionBody, cleanScopes, claimsAllowed, ASSERTION_DEFAULT_TTL_MS, ASSERTION_MAX_TTL_MS } from './assertion.js'
26
26
  import { pubkeyId as pubkeyIdOf, signWithDevice } from './capabilities.js'
27
- import { enrollDevice as remoteEnroll, requestSign as remoteSign, requestStore as remoteStore, requestDevices as remoteDevices, requestRenew as remoteRenew, requestAdmin as remoteAdmin, requestApproval as remoteApproval, requestRenounce as remoteRenounce, checkMembership as remoteCheck } from './remote.js'
27
+ import { enrollDevice as remoteEnroll, requestSign as remoteSign, requestStore as remoteStore, requestDevices as remoteDevices, requestRenew as remoteRenew, requestAdmin as remoteAdmin, requestApproval as remoteApproval, requestRenounce as remoteRenounce, checkMembership as remoteCheck, registerLocalVault } from './remote.js'
28
28
 
29
29
  export const KEY_STORAGE = 'dotrino.identity.keypair'
30
30
  export const ENC_KEY_STORAGE = 'dotrino.identity.enc-keypair'
@@ -791,7 +791,43 @@ 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
+
795
+ /**
796
+ * LA BÓVEDA QUE CORRE AQUÍ MISMO. Cuando esta pestaña o esta extensión ES la bóveda de la
797
+ * cuenta (`startBrowserVault` de `@dotrino/vault`), administrarla y aprobar sus pedidos se
798
+ * hace con el MISMO protocolo que contra el binario, firmado con la llave del perfil y con
799
+ * el papel que la bóveda se da a sí misma; el viaje lo hace el camino local de
800
+ * `remote.js`, sin proxio.
801
+ *
802
+ * Vive en memoria y NO se guarda como emparejamiento: este navegador no tiene «una bóveda
803
+ * aparte», y fingirlo haría creer, por ejemplo, que lo suyo está respaldado en otro sitio.
804
+ * Lo pone quien levanta la bóveda (`setSelfVault`) y lo quita al pararla.
805
+ */
806
+ let selfLink = null
807
+ let selfLinkOff = null
808
+ /** El enlace con el que administrar: el emparejamiento si lo hay, y si no, la bóveda local. */
809
+ const adminLink = () => {
810
+ const v = loadVaultCert(); const device = loadVaultDevice()
811
+ if (v?.cert && device) return { v, device, self: false }
812
+ if (selfLink && publickeyJwkStr && masterKey()) {
813
+ return {
814
+ v: { cert: selfLink.cert, master: publickeyJwkStr, proxy: selfLink.proxy },
815
+ device: { publickey: publickeyJwkStr, privateKey: masterKey(), ...(pageMachine ? { machine: pageMachine } : {}) },
816
+ self: true
817
+ }
818
+ }
819
+ return null
820
+ }
821
+ // DÓNDE CORRE ESTE APARATO (`machine.js`). Lo dice la PÁGINA al conectar (`setMachine`),
822
+ // porque el iframe no ve si está dentro de una extensión o de la app nativa, y viaja con
823
+ // cada pedido firmado a la bóveda para que su lista lo enseñe al día. Sin página que lo
824
+ // diga (Node, un cliente viejo) no se manda nada: mejor ningún dato que uno equivocado.
825
+ let pageMachine = null
794
826
  const loadVaultDevice = () => {
827
+ const d = loadVaultDeviceKey()
828
+ return d && pageMachine ? { ...d, machine: pageMachine } : d
829
+ }
830
+ const loadVaultDeviceKey = () => {
795
831
  try {
796
832
  const d = JSON.parse(kv.getItem(VAULT_DEVICE_STORAGE) || 'null')
797
833
  if (!d) return null
@@ -1293,7 +1329,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
1293
1329
  * cert contra la bóveda y entra a su cuenta. Aparte para que la decisión de CUÁL cuenta y
1294
1330
  * el deshacerla si esto falla vivan juntos arriba, y aquí solo quede el trámite.
1295
1331
  */
1296
- async function pairWithVault ({ qr, label = '' }) {
1332
+ async function pairWithVault ({ qr, label = '', machine = undefined }) {
1297
1333
  // Usa la PROPIA llave de identidad de este navegador como dispositivo: el cert delega
1298
1334
  // TU identidad (P) desde la maestra M → una sola identidad (signData/identify/cert = P).
1299
1335
  // La privada es la CryptoKey del perfil (no extractable): se pasa como `privateKey`
@@ -1307,7 +1343,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
1307
1343
  const continuity = (mine && mine.members.length === 1 && !isPendingJoin())
1308
1344
  ? await Acta.makeContinuity({ member: publickeyJwkStr, from: mine.profileId, privateKey: masterKey() })
1309
1345
  : 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 }) })
1346
+ 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
1347
  kv.setItem(VAULT_DEVICE_STORAGE, JSON.stringify({ useIdentityKey: true, publickey: publickeyJwkStr }))
1312
1348
  kv.setItem(VAULT_CERT_STORAGE, JSON.stringify({ cert: res.cert, master: res.master, proxy: res.proxy, deviceId: res.deviceId, pairedAt: Date.now() }))
1313
1349
  // Conectarse a una bóveda es ENTRAR A SU CUENTA: el acta viene con el cert.
@@ -2560,6 +2596,8 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
2560
2596
  isService: !!m.cn,
2561
2597
  caps: Acta.effectiveCaps(acta, m.pub, pend),
2562
2598
  addedAt: m.addedAt || null,
2599
+ // Dónde corre, según dijo al emparejarse (`machine.js`). `null` si entró antes.
2600
+ machine: m.machine || null,
2563
2601
  isMe: m.pub === publickeyJwkStr,
2564
2602
  isMaster: Acta.canSeal(acta, m.pub),
2565
2603
  // La llave de CIFRADO del miembro y, derivado, si se le puede envolver algo.
@@ -2605,7 +2643,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
2605
2643
  * Admite un miembro (solo el master). El cert lo emite quien llama, antes o después.
2606
2644
  * `continuity`: si esa identidad ya existía por su cuenta, su puente firmado (F3).
2607
2645
  */
2608
- async admitMember ({ pub, encPub = null, label = '', cn = null, caps = null, cert = null, continuity = null } = {}) {
2646
+ async admitMember ({ pub, encPub = null, label = '', cn = null, caps = null, cert = null, continuity = null, machine = null } = {}) {
2609
2647
  // `cn`: si viene, este miembro es un SERVICIO: abre el cajón de secretos de ESE nombre
2610
2648
  // por llevarlo, sin permiso aparte, y no ve nada más del usuario. Sin `cn` es un dispositivo.
2611
2649
  const finales = caps || (cn ? [] : ['store', 'read'])
@@ -2622,7 +2660,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
2622
2660
  const mine = encPub ? await myCek() : null
2623
2661
  if (mine) wrap = { op: 'wrap', gen: mine.gen, pub, wrap: await Content.wrapForMember({ cek: mine.cek, memberEncPub: encPub }) }
2624
2662
  } catch (_) { wrap = null }
2625
- const acta = await sealChanges([{ op: 'admit', member: { pub, encPub, label, cn, caps: finales, cert, continuity } }, ...(wrap ? [wrap] : [])])
2663
+ const acta = await sealChanges([{ op: 'admit', member: { pub, encPub, label, cn, caps: finales, cert, continuity, machine } }, ...(wrap ? [wrap] : [])])
2626
2664
  return { ok: true, seq: acta.seq, wrapped: !!wrap }
2627
2665
  },
2628
2666
 
@@ -2898,8 +2936,8 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
2898
2936
  * master: si esta cuenta ya la manda otra bóveda, no hay nada que
2899
2937
  * regalar y falla en voz alta.
2900
2938
  */
2901
- async vaultPair ({ qr, label = '', join = 'current' }) {
2902
- if (join === 'adopt') return handlers.vaultAdopt({ qr, label })
2939
+ async vaultPair ({ qr, label = '', join = 'current', machine = undefined }) {
2940
+ if (join === 'adopt') return handlers.vaultAdopt({ qr, label, machine })
2903
2941
  // La cuenta abierta AQUÍ y la que se cree para este intento. `born` es la que hay que
2904
2942
  // tirar si el emparejamiento no llega a término: nació para él y no tiene nada dentro.
2905
2943
  const from = currentPid
@@ -2929,7 +2967,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
2929
2967
  }
2930
2968
  }
2931
2969
  try {
2932
- return await pairWithVault({ qr, label })
2970
+ return await pairWithVault({ qr, label, machine })
2933
2971
  } catch (e) {
2934
2972
  // El intento falló (código vencido, la bóveda dijo que no, se agotó la espera): la
2935
2973
  // cuenta que nació para él se va con él. Si no, cada reintento dejaba una cuenta
@@ -2953,7 +2991,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
2953
2991
  * bóveda, este aparato no puede regalar lo que no tiene; el traspaso se hace desde la
2954
2992
  * que manda hoy.
2955
2993
  */
2956
- async vaultAdopt ({ qr, label = '' } = {}) {
2994
+ async vaultAdopt ({ qr, label = '', machine = undefined } = {}) {
2957
2995
  const mine = loadActa()
2958
2996
  if (!mine) throw new Error('this device has no account to hand over yet')
2959
2997
  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 +3001,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
2963
3001
  qr,
2964
3002
  device,
2965
3003
  intent: 'adopt',
3004
+ machine,
2966
3005
  profileId: mine.profileId,
2967
3006
  encPub: encPublickeyJwkStr,
2968
3007
  label: label || me?.nickname || '',
@@ -3009,6 +3048,15 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
3009
3048
  return { ok: true, pending: true }
3010
3049
  },
3011
3050
 
3051
+ /**
3052
+ * La página dice dónde corre (`machine.js`); se manda con los pedidos a la bóveda.
3053
+ * @param {any} [args]
3054
+ */
3055
+ async setMachine ({ machine } = {}) {
3056
+ pageMachine = Acta.cleanMachine(machine)
3057
+ return { ok: true }
3058
+ },
3059
+
3012
3060
  async vaultStatus () {
3013
3061
  const v = loadVaultCert()
3014
3062
  if (!v?.cert) return { paired: false }
@@ -3076,8 +3124,10 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
3076
3124
  * master, en su máquina.
3077
3125
  */
3078
3126
  async vaultAdmin ({ op, ...rest } = {}) {
3079
- const v = loadVaultCert(); const device = loadVaultDevice()
3080
- if (!v?.cert || !device) throw new Error('this device is not paired with a vault')
3127
+ const link = adminLink()
3128
+ if (!link) throw new Error('this device is not paired with a vault')
3129
+ const { v, device } = link
3130
+ if (link.self) return remoteAdmin({ master: v.master, proxy: v.proxy, device, cert: v.cert, op, ...rest })
3081
3131
  maybeRenewVaultCert()
3082
3132
  try { return await remoteAdmin({ master: v.master, proxy: v.proxy, device, cert: v.cert, op, ...rest, onRevoked: wipeVaultLink }) }
3083
3133
  catch (e) { return handleVaultError(e) }
@@ -3093,8 +3143,14 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
3093
3143
  */
3094
3144
  async vaultApprovals ({ op, id, profile = null, notify } = {}) {
3095
3145
  if (profile && profile !== currentPid) return approvalsDeOtroPerfil(profile, { op, id })
3096
- const v = loadVaultCert(); const device = loadVaultDevice()
3097
- if (!v?.cert || !device) throw new Error('this device is not paired with a vault')
3146
+ const link = adminLink()
3147
+ if (!link) throw new Error('this device is not paired with a vault')
3148
+ const { v, device } = link
3149
+ if (link.self) {
3150
+ const r = await remoteApproval({ master: v.master, proxy: v.proxy, device, cert: v.cert, op, id, notify })
3151
+ if (Array.isArray(r?.items)) return { ...r, items: await abrirContextos(r.items, currentPid) }
3152
+ return r
3153
+ }
3098
3154
  maybeRenewVaultCert()
3099
3155
  const ask = (cert) => remoteApproval({ master: v.master, proxy: v.proxy, device, cert, op, id, notify, onRevoked: wipeVaultLink })
3100
3156
  try {
@@ -3143,12 +3199,14 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
3143
3199
  try { maybeRenewVaultCert() } catch (_) {}
3144
3200
  const perfiles = loadProfiles()
3145
3201
  const salida = await Promise.all(perfiles.map(async (p) => {
3146
- const v = p.id === currentPid ? loadVaultCert() : vaultCertOf(p.id)
3202
+ // La activa: su emparejamiento, o la bóveda que corre aquí mismo si no tiene otro.
3203
+ const propia = p.id === currentPid && !loadVaultCert() ? adminLink() : null
3204
+ const v = p.id === currentPid ? (loadVaultCert() || propia?.v || null) : vaultCertOf(p.id)
3147
3205
  if (!v?.cert) return null // sin bóveda: no es asunto suyo
3148
3206
  if (!(v.cert.scope || []).includes('vault:approve')) return null // no aprueba: tampoco
3149
3207
  const base = { profile: p.id, name: p.name || '', current: p.id === currentPid }
3150
3208
  let device = null
3151
- try { device = p.id === currentPid ? loadVaultDevice() : await signerForProfile(p.id) } catch (e) {
3209
+ try { device = p.id === currentPid ? (propia?.device || loadVaultDevice()) : await signerForProfile(p.id) } catch (e) {
3152
3210
  return { ...base, items: [], error: e?.code || 'no-key' }
3153
3211
  }
3154
3212
  if (!device) return { ...base, items: [], error: 'no-key' }
@@ -3189,6 +3247,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
3189
3247
 
3190
3248
  /** ¿Puede ESTE dispositivo aprobar pedidos? Mismo criterio que `canAdminVault`. */
3191
3249
  async canApproveVault () {
3250
+ if (!loadVaultCert() && selfLink) return (adminLink()?.v.cert.scope || []).includes('vault:approve')
3192
3251
  if (certDesfasadoDelActa()) { try { await renovarCert() } catch (_) {} }
3193
3252
  const v = loadVaultCert()
3194
3253
  return !!v?.cert && (v.cert.scope || []).includes('vault:approve')
@@ -3204,6 +3263,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
3204
3263
  // renovarlo aquí mismo. Disparar la renovación y contestar «no» dejaba la consola
3205
3264
  // escondida hasta la siguiente visita, y el dueño —que acababa de dar el permiso— no
3206
3265
  // tenía forma de saber que solo faltaba recargar.
3266
+ if (!loadVaultCert() && selfLink) return (adminLink()?.v.cert.scope || []).includes('vault:admin')
3207
3267
  if (certDesfasadoDelActa()) { try { await renovarCert() } catch (_) {} }
3208
3268
  const v = loadVaultCert()
3209
3269
  return !!v?.cert && (v.cert.scope || []).includes('vault:admin')
@@ -3212,6 +3272,13 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
3212
3272
  // Lista (solo lectura) de dispositivos enrolados en tu vault.
3213
3273
  async listVaultDevices () {
3214
3274
  const v = loadVaultCert(); const device = loadVaultDevice()
3275
+ // ESTE NAVEGADOR ES LA BÓVEDA: la lista sale de ella, por el camino local, con lo mismo
3276
+ // que da el binario (qué versión corre cada aparato, dónde corre, si le llegan avisos).
3277
+ // El acta es la nuestra: no hay nada que adoptar.
3278
+ if ((!v?.cert || !device) && adminLink()?.self) {
3279
+ const self = adminLink()
3280
+ return remoteDevices({ master: self.v.master, proxy: self.v.proxy, device: self.device, cert: self.v.cert, sinceSeq: loadActa()?.seq ?? 0 })
3281
+ }
3215
3282
  if (!v?.cert || !device) {
3216
3283
  // SOLO SE AVISA SI ES RARO. Un aparato que manda en su propia cuenta —o que aún no
3217
3284
  // entró en ninguna bóveda— no está paired y no pasa absolutamente nada: avisar ahí
@@ -3619,6 +3686,19 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
3619
3686
  * `handlers`) porque ninguna aplicación tiene por qué poder instalar una identidad.
3620
3687
  */
3621
3688
  adoptLogin,
3689
+ /**
3690
+ * La bóveda que corre aquí mismo se presenta (`startBrowserVault`): `{ cert, proxy, open }`
3691
+ * —`open` es su canal local, ver `registerLocalVault` en `remote.js`—, o `null` al
3692
+ * pararse (ver `adminLink`). En el núcleo y no en `handlers`: ninguna
3693
+ * aplicación tiene por qué poder decir dónde está la bóveda de esta cuenta.
3694
+ */
3695
+ setSelfVault (self = null) {
3696
+ if (self && (!self.cert || typeof self.proxy !== 'string' || typeof self.open !== 'function')) throw new Error('setSelfVault: cert, proxy and open are required')
3697
+ try { selfLinkOff?.() } catch (_) {}
3698
+ selfLinkOff = null
3699
+ selfLink = self ? { cert: self.cert, proxy: self.proxy } : null
3700
+ if (self && publickeyJwkStr) selfLinkOff = registerLocalVault(publickeyJwkStr, self.open)
3701
+ },
3622
3702
  onSyncStatus (fn) { if (sync) sync.onStatus(fn) },
3623
3703
  onVaultEvent (fn) { vaultListeners.add(fn); return () => vaultListeners.delete(fn) }
3624
3704
  }