@dotrino/identity 0.46.0 → 0.48.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 +1 -1
- package/src/index.js +12 -2
- package/vault/core.js +49 -8
- package/vault/remote.js +21 -2
- package/vault/vault.js +14 -1
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -439,8 +439,18 @@ export class Identity {
|
|
|
439
439
|
async selfVaultApprove (deviceId, code) { return this._call('selfVaultApprove', { deviceId, code }) }
|
|
440
440
|
/** Rechaza una solicitud de emparejamiento pendiente. */
|
|
441
441
|
async selfVaultReject (deviceId) { return this._call('selfVaultReject', { deviceId }) }
|
|
442
|
-
/**
|
|
443
|
-
|
|
442
|
+
/**
|
|
443
|
+
* QUITA una máquina/agente enrolado en esta bóveda. Se le pasa **el aparato**
|
|
444
|
+
* (`{ sub }`, su llave): sale del acta y se le retiran todos sus certificados.
|
|
445
|
+
*
|
|
446
|
+
* Un `nonce` suelto (string, o `{ nonce }`) retira UN certificado y deja al aparato
|
|
447
|
+
* dentro del acta: eso no es quitarlo, y quien queda así ya no recibe nunca el aviso
|
|
448
|
+
* de expulsión. Se acepta por compatibilidad, no como la forma normal.
|
|
449
|
+
*/
|
|
450
|
+
async selfVaultRevoke (target) {
|
|
451
|
+
const p = typeof target === 'string' ? { nonce: target } : { sub: target?.sub, nonce: target?.nonce }
|
|
452
|
+
return this._call('selfVaultRevoke', p)
|
|
453
|
+
}
|
|
444
454
|
/** Presencia online (ping/pong) de las máquinas enroladas. Devuelve { online: [pubkeys] }. */
|
|
445
455
|
async selfVaultProbe (pubkeys) { return this._call('selfVaultProbe', { pubkeys }, 10000) }
|
|
446
456
|
/** Suscribe a eventos del self-vault ('selfVault'): { running?, pending?, error? }. */
|
package/vault/core.js
CHANGED
|
@@ -342,19 +342,60 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
342
342
|
|
|
343
343
|
/**
|
|
344
344
|
* BORRADO por revocación. Solo lo dispara un `vault.revoked` FIRMADO por la maestra
|
|
345
|
-
* pineada (lo verifica `remote.js` antes de llamar aquí).
|
|
346
|
-
*
|
|
345
|
+
* pineada (lo verifica `remote.js` antes de llamar aquí).
|
|
346
|
+
*
|
|
347
|
+
* Se va la CUENTA ENTERA, no solo el enlace. Una cuenta que vivía en una bóveda y de la
|
|
348
|
+
* que te echaron no es «una cuenta sin bóveda»: no es nada. Dejarla ahí —sin acta, sin
|
|
349
|
+
* certificado, con su llave— era dejar un cascarón que seguía saliendo en el conmutador
|
|
350
|
+
* de perfiles, con su nombre y su foto, sin poder hacer nada y sin que nadie supiera qué
|
|
351
|
+
* era ni cómo quitarlo.
|
|
352
|
+
*
|
|
353
|
+
* El aparato NUNCA se queda sin cuenta utilizable, que es la otra mitad: si había otras,
|
|
354
|
+
* se pasa a una de ellas; si esa era la única, se estrena una vacía. Así al terminar hay
|
|
355
|
+
* exactamente lo que tiene que haber: un dispositivo con su cuenta, listo para usarse o
|
|
356
|
+
* para volver a conectarse a una bóveda.
|
|
357
|
+
*
|
|
358
|
+
* Los pasos van EN ESTE ORDEN a propósito:
|
|
359
|
+
* 1. fuera el enlace y el acta (deja de poder hablar con la bóveda y de enseñar el
|
|
360
|
+
* perfil del que lo echaron);
|
|
361
|
+
* 2. 'revoked' → `@dotrino/store` borra el store de ESE perfil (apunta al id que ya
|
|
362
|
+
* tenía fijado, así que da igual lo que hagamos después con el perfil activo);
|
|
363
|
+
* 3. se borra la cuenta y se deja otra puesta;
|
|
364
|
+
* 4. 'account-removed' → la app RECARGA (multi-perfil no es reactivo, por diseño).
|
|
347
365
|
*/
|
|
348
366
|
const wipeVaultLink = () => {
|
|
349
367
|
try { kv.removeItem(VAULT_CERT_STORAGE); kv.removeItem(VAULT_DEVICE_STORAGE) } catch (_) {}
|
|
350
|
-
// Y el ACTA. Sin esto el aparato borraba su enlace con la bóveda pero seguía
|
|
351
|
-
// enseñando el perfil del que acababan de echarlo —con sus miembros y sus permisos—,
|
|
352
|
-
// porque la copia local se quedaba ahí y ya no podía refrescarla (está revocado). El
|
|
353
|
-
// dueño lo describió exacto: «el device ni por enterado de que está fuera».
|
|
354
|
-
// Queda como lo que es: un dispositivo sin cuenta, que puede crear la suya o
|
|
355
|
-
// emparejarse otra vez.
|
|
356
368
|
try { kv.removeItem(ACTA_STORAGE) } catch (_) {}
|
|
357
369
|
emitVault({ phase: 'revoked' })
|
|
370
|
+
// Sin `await`: quien llama es un manejador de mensajes que no espera nada (y si algo
|
|
371
|
+
// aquí revienta, el enlace y el acta ya se fueron, que es lo que no puede quedar).
|
|
372
|
+
removeThisAccount().catch(() => {})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Borra la cuenta de ESTE dispositivo y deja otra activa (existente o recién creada).
|
|
377
|
+
* Se usa al ser expulsado; ver `wipeVaultLink`.
|
|
378
|
+
*/
|
|
379
|
+
let removingAccount = false
|
|
380
|
+
async function removeThisAccount () {
|
|
381
|
+
// UNA VEZ. Puede haber varias peticiones en vuelo y a todas les llega el mismo aviso;
|
|
382
|
+
// sin este cerrojo, la segunda borraría la cuenta RECIÉN CREADA (para entonces
|
|
383
|
+
// `currentPid` ya es la nueva) en vez de la que echaron.
|
|
384
|
+
if (removingAccount) return
|
|
385
|
+
removingAccount = true
|
|
386
|
+
const gone = currentPid
|
|
387
|
+
// Si esa era la única cuenta hay que estrenar otra ANTES de borrarla: `deleteProfile`
|
|
388
|
+
// se niega a dejar el dispositivo sin ninguna, y con razón. Crear ya deja la nueva
|
|
389
|
+
// activa; si había otras, es el propio `deleteProfile` quien pasa a la primera.
|
|
390
|
+
const created = loadProfiles().filter((p) => p.id !== gone).length === 0
|
|
391
|
+
try {
|
|
392
|
+
if (created) await handlers.createProfile({ name: '' })
|
|
393
|
+
await handlers.deleteProfile({ id: gone })
|
|
394
|
+
} catch (e) {
|
|
395
|
+
emitVault({ phase: 'account-removed', removed: gone, current: currentPid, error: e?.message || String(e) })
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
emitVault({ phase: 'account-removed', removed: gone, current: currentPid, created })
|
|
358
399
|
}
|
|
359
400
|
|
|
360
401
|
/**
|
package/vault/remote.js
CHANGED
|
@@ -223,6 +223,13 @@ export async function requestSign ({ master, proxy, device, cert, payload, onRev
|
|
|
223
223
|
return { signature: res.signature, publickey: res.publickey }
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Cuánto se espera el aviso FIRMADO de expulsión después de que la bóveda conteste
|
|
228
|
+
* «revoked». Corto a propósito: es el tiempo de un mensaje que ya viene en camino, no una
|
|
229
|
+
* espera de verdad.
|
|
230
|
+
*/
|
|
231
|
+
const REVOKE_GRACE_MS = 1500
|
|
232
|
+
|
|
226
233
|
/**
|
|
227
234
|
* Helper genérico: una RPC al vault firmada por D + cert, esperando `okType`.
|
|
228
235
|
*
|
|
@@ -240,6 +247,7 @@ async function vaultRpc ({ master, proxy, device, cert, acta = null, sendType, o
|
|
|
240
247
|
const signed = { ...data, publickey: device.publickey, ts: Date.now() }
|
|
241
248
|
const { signature } = await signWithDevice({ privateJwk: device.privateJwk, privateKey: device.privateKey, publickey: device.publickey, data: signed })
|
|
242
249
|
const pending = new Promise((resolve, reject) => {
|
|
250
|
+
let graceTimer = null
|
|
243
251
|
const off = client.on('message', (_f, p) => {
|
|
244
252
|
if (!p || typeof p !== 'object') return
|
|
245
253
|
if (p.type === MSG.REVOKED) {
|
|
@@ -249,10 +257,21 @@ async function vaultRpc ({ master, proxy, device, cert, acta = null, sendType, o
|
|
|
249
257
|
return
|
|
250
258
|
}
|
|
251
259
|
if (p.type === okType) { cleanup(); resolve(p) }
|
|
252
|
-
else if (p.type === 'vault.error') {
|
|
260
|
+
else if (p.type === 'vault.error') {
|
|
261
|
+
// «Te echaron» llega en DOS mensajes: este error (que no va firmado, así que no
|
|
262
|
+
// puede borrar nada — wipe-DoS) y el `vault.revoked` FIRMADO, que es el único que
|
|
263
|
+
// sí. Cerrar el socket al recibir el primero era llegar a cerrar la puerta justo
|
|
264
|
+
// antes que el segundo, y entonces el aparato se quedaba con la cuenta puesta
|
|
265
|
+
// hasta vaya a saber cuándo. Se le da un respiro corto para recogerlo.
|
|
266
|
+
if (/\brevoked\b/.test(p.error || '') && !graceTimer) {
|
|
267
|
+
graceTimer = setTimeout(() => { cleanup(); reject(new Error(p.error)) }, REVOKE_GRACE_MS)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
cleanup(); reject(new Error(p.error))
|
|
271
|
+
}
|
|
253
272
|
})
|
|
254
273
|
const t = setTimeout(() => { cleanup(); reject(new Error('the vault did not reply (is it running?)')) }, timeoutMs)
|
|
255
|
-
const cleanup = () => { off(); clearTimeout(t) }
|
|
274
|
+
const cleanup = () => { off(); clearTimeout(t); clearTimeout(graceTimer) }
|
|
256
275
|
})
|
|
257
276
|
client.sendByPubkey(master, { type: sendType, data: signed, signature, cert })
|
|
258
277
|
return await pending
|
package/vault/vault.js
CHANGED
|
@@ -239,7 +239,20 @@ import { pubkeyId } from './capabilities.js'
|
|
|
239
239
|
daemon.reject(deviceId)
|
|
240
240
|
return { ok: true }
|
|
241
241
|
},
|
|
242
|
-
|
|
242
|
+
/**
|
|
243
|
+
* QUITAR UN APARATO cuando la bóveda es ESTE navegador. Por `sub` (su llave): sale del
|
|
244
|
+
* acta y se le retiran todos los certificados, que son las dos caras del mismo acto.
|
|
245
|
+
*
|
|
246
|
+
* Con `nonce` a secas solo cae UN papel y el aparato sigue siendo miembro — un
|
|
247
|
+
* fantasma en la lista al que además ya nunca le llega el aviso de expulsión, porque
|
|
248
|
+
* mientras siga en el acta un papel retirado significa «renueva», no «estás fuera».
|
|
249
|
+
* Se conserva para quien de verdad quiera retirar un certificado suelto.
|
|
250
|
+
*/
|
|
251
|
+
selfVaultRevoke: async ({ sub, nonce }) => {
|
|
252
|
+
if (sub) {
|
|
253
|
+
if (daemon?.revokeDevice) return daemon.revokeDevice(sub)
|
|
254
|
+
return handlers.revokeDevice({ sub })
|
|
255
|
+
}
|
|
243
256
|
if (daemon) return daemon.revoke(nonce)
|
|
244
257
|
return handlers.revokeDelegation({ nonce })
|
|
245
258
|
},
|