@dotrino/identity 0.92.0 → 0.94.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.d.ts +5 -2
- package/src/index.js +6 -2
- package/vault/core.js +47 -20
- package/vault/vendor/vault/VERSION.txt +1 -1
- package/vault/vendor/vault/enroll.js +39 -12
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -126,8 +126,11 @@ export class Identity {
|
|
|
126
126
|
requestAssertion (args: { audience: string; nonce: string; scopes?: AssertionScope[]; ttlMs?: number; onBehalfOf?: string }): Promise<Assertion>
|
|
127
127
|
/** Qué le has concedido a cada aplicación (permiso por origen). */
|
|
128
128
|
listGrants (): Promise<Array<{ origin: string; scopes: AssertionScope[]; at: number; lastUsed: number; onBehalfOf?: string }>>
|
|
129
|
-
/**
|
|
130
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Retirar lo concedido a una aplicación: la próxima vez que pida, se vuelve a preguntar.
|
|
131
|
+
* `onBehalfOf` identifica a una aplicación que entra por el puente (el mismo origen para todas).
|
|
132
|
+
*/
|
|
133
|
+
revokeGrant (origin: string, onBehalfOf?: string): Promise<{ ok: boolean }>
|
|
131
134
|
setMyNickname (nickname: string): Promise<{ me: Me }>
|
|
132
135
|
getEncryptionPubkey (): Promise<string>
|
|
133
136
|
encrypt (recipients: EncryptRecipient[], plaintext: string): Promise<EnvelopeV1>
|
package/src/index.js
CHANGED
|
@@ -274,8 +274,12 @@ export class Identity {
|
|
|
274
274
|
*/
|
|
275
275
|
/** Qué le has concedido a cada aplicación. Sin esto, conceder no significaría nada. */
|
|
276
276
|
async listGrants () { return this._call('listGrants') }
|
|
277
|
-
/**
|
|
278
|
-
|
|
277
|
+
/**
|
|
278
|
+
* Retirar lo concedido a una aplicación: la próxima vez que pida, se vuelve a preguntar.
|
|
279
|
+
* Las que entran por el puente llegan todas desde el mismo origen, así que a esas se las
|
|
280
|
+
* nombra con `onBehalfOf` (el que trae su fila de `listGrants`).
|
|
281
|
+
*/
|
|
282
|
+
async revokeGrant (origin, onBehalfOf) { return this._call('revokeGrant', { origin, ...(onBehalfOf ? { onBehalfOf } : {}) }) }
|
|
279
283
|
|
|
280
284
|
async requestAssertion ({ audience, nonce, scopes, ttlMs, onBehalfOf } = {}) {
|
|
281
285
|
// ESPERA LO QUE TARDE UNA PERSONA. Los cinco segundos de siempre valen para una
|
package/vault/core.js
CHANGED
|
@@ -725,13 +725,34 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
725
725
|
// `acta.js`, que es puro y está probado aparte.
|
|
726
726
|
// ----- PERMISO POR ORIGEN: qué le concedió el usuario a cada aplicación -----
|
|
727
727
|
//
|
|
728
|
-
// `{ [
|
|
729
|
-
// perfil cambia lo concedido: lo que le diste a una aplicación
|
|
730
|
-
// no vale para la personal.
|
|
731
|
-
|
|
728
|
+
// `{ [grantKey]: { scopes: [...], at, lastUsed, onBehalfOf? } }`. Vive en el kv del
|
|
729
|
+
// PERFIL, así que cambiar de perfil cambia lo concedido: lo que le diste a una aplicación
|
|
730
|
+
// desde tu cuenta de trabajo no vale para la personal.
|
|
731
|
+
//
|
|
732
|
+
// LA CLAVE ES EL ORIGEN Y, SI PIDE POR OTRO, TAMBIÉN EN NOMBRE DE QUIÉN (dueño,
|
|
733
|
+
// 2026-09-17). Hasta 0.92 era solo el origen, y todas las aplicaciones que entran por el
|
|
734
|
+
// puente OIDC llegan desde el MISMO: `sso.dotrino.com`. Lo que el usuario le concedía a
|
|
735
|
+
// una lo heredaban todas las demás sin que saliera el panel, y en «dónde se usó mi
|
|
736
|
+
// identidad» se veían como una sola fila con el nombre de la última.
|
|
737
|
+
//
|
|
738
|
+
// Separar por `onBehalfOf` no le da a nadie más de lo que tenía: el nombre lo pone el
|
|
739
|
+
// origen, así que un origen que mintiera solo conseguiría una concesión aparte —vacía—,
|
|
740
|
+
// nunca la de otro origen. `|` no aparece en un origen, así que la clave no es ambigua.
|
|
741
|
+
const grantKey = (origin, onBehalfOf) => onBehalfOf ? `${origin}|${onBehalfOf}` : origin
|
|
742
|
+
const behalfName = (onBehalfOf) => onBehalfOf ? String(onBehalfOf).slice(0, 60) : ''
|
|
743
|
+
const loadGrants = () => {
|
|
744
|
+
let g
|
|
745
|
+
try { g = JSON.parse(kv.getItem(GRANTS_STORAGE) || '{}') } catch (_) { return {} }
|
|
746
|
+
// MIGRACIÓN DECLARADA (identity 0.93.0) — quitar después del 2026-10-17.
|
|
747
|
+
// Una entrada guardada bajo el origen A SECAS pero con `onBehalfOf` es de antes de 0.93:
|
|
748
|
+
// la compartían todas las aplicaciones del puente, así que no se puede atribuir a
|
|
749
|
+
// ninguna. Se tira, y cada aplicación vuelve a preguntar — que es el lado seguro.
|
|
750
|
+
for (const k of Object.keys(g)) if (!k.includes('|') && g[k]?.onBehalfOf) delete g[k]
|
|
751
|
+
return g
|
|
752
|
+
}
|
|
732
753
|
const saveGrants = (g) => { try { kv.setItem(GRANTS_STORAGE, JSON.stringify(g)) } catch (_) {} }
|
|
733
|
-
/** Lo concedido a un origen, hoy. */
|
|
734
|
-
const grantedTo = (
|
|
754
|
+
/** Lo concedido a un origen (y a nombre de quién pide), hoy. */
|
|
755
|
+
const grantedTo = (key) => (loadGrants()[key]?.scopes) || []
|
|
735
756
|
|
|
736
757
|
const loadActa = () => { try { return JSON.parse(kv.getItem(ACTA_STORAGE) || 'null') } catch (_) { return null } }
|
|
737
758
|
const saveActa = (a) => kv.setItem(ACTA_STORAGE, JSON.stringify(a))
|
|
@@ -1537,6 +1558,8 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1537
1558
|
*/
|
|
1538
1559
|
async function consentFor (origin, pedidos, onBehalfOf = null) {
|
|
1539
1560
|
const org = String(origin || '').trim()
|
|
1561
|
+
const behalf = behalfName(onBehalfOf)
|
|
1562
|
+
const key = grantKey(org, behalf)
|
|
1540
1563
|
// APUNTA CUÁNDO SE USÓ. Sin esto, «dónde se usó mi identidad» solo puede decir qué
|
|
1541
1564
|
// concediste, no si sigue usándose — y eso es lo que hace que uno se decida a retirar
|
|
1542
1565
|
// un permiso que ya no hace falta. Se apunta aunque solo se pida el mínimo: entrar es
|
|
@@ -1544,8 +1567,8 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1544
1567
|
const marcarUso = () => {
|
|
1545
1568
|
if (!org) return
|
|
1546
1569
|
const g = loadGrants()
|
|
1547
|
-
const prev = g[
|
|
1548
|
-
g[
|
|
1570
|
+
const prev = g[key] || { scopes: [], at: Date.now() }
|
|
1571
|
+
g[key] = { ...prev, lastUsed: Date.now(), ...(behalf ? { onBehalfOf: behalf } : {}) }
|
|
1549
1572
|
saveGrants(g)
|
|
1550
1573
|
}
|
|
1551
1574
|
const base = pedidos.filter((s) => s === 'id:whoami')
|
|
@@ -1555,7 +1578,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1555
1578
|
// concede nada más que el mínimo. Es el caso de Node y el de una llamada interna.
|
|
1556
1579
|
if (!org) return base.length ? base : ['id:whoami']
|
|
1557
1580
|
|
|
1558
|
-
const yaTiene = grantedTo(
|
|
1581
|
+
const yaTiene = grantedTo(key)
|
|
1559
1582
|
const faltan = extra.filter((s) => !yaTiene.includes(s))
|
|
1560
1583
|
if (!faltan.length) { marcarUso(); return pedidos }
|
|
1561
1584
|
|
|
@@ -1565,17 +1588,17 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1565
1588
|
return conocidos.length ? conocidos : ['id:whoami']
|
|
1566
1589
|
}
|
|
1567
1590
|
let ok = false
|
|
1568
|
-
try { ok = !!(await askConsent({ origin: org, scopes: faltan, already: yaTiene, onBehalfOf })) } catch (_) { ok = false }
|
|
1591
|
+
try { ok = !!(await askConsent({ origin: org, scopes: faltan, already: yaTiene, onBehalfOf: behalf || null })) } catch (_) { ok = false }
|
|
1569
1592
|
if (!ok) {
|
|
1570
1593
|
const conocidos = [...base, ...extra.filter((s) => yaTiene.includes(s))]
|
|
1571
1594
|
marcarUso()
|
|
1572
1595
|
return conocidos.length ? conocidos : ['id:whoami']
|
|
1573
1596
|
}
|
|
1574
1597
|
const g = loadGrants()
|
|
1575
|
-
g[
|
|
1598
|
+
g[key] = {
|
|
1576
1599
|
scopes: [...new Set([...yaTiene, ...faltan])].sort(),
|
|
1577
|
-
at: (g[
|
|
1578
|
-
...(
|
|
1600
|
+
at: (g[key]?.at) || Date.now(), lastUsed: Date.now(),
|
|
1601
|
+
...(behalf ? { onBehalfOf: behalf } : {})
|
|
1579
1602
|
}
|
|
1580
1603
|
saveGrants(g)
|
|
1581
1604
|
return pedidos
|
|
@@ -1846,18 +1869,22 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1846
1869
|
*/
|
|
1847
1870
|
async listGrants () {
|
|
1848
1871
|
const g = loadGrants()
|
|
1849
|
-
return Object.entries(g).map(([
|
|
1850
|
-
origin, scopes: v?.scopes || [], at: v?.at || 0,
|
|
1872
|
+
return Object.entries(g).map(([key, v]) => ({
|
|
1873
|
+
origin: key.split('|')[0], scopes: v?.scopes || [], at: v?.at || 0,
|
|
1851
1874
|
lastUsed: v?.lastUsed || v?.at || 0,
|
|
1852
1875
|
...(v?.onBehalfOf ? { onBehalfOf: v.onBehalfOf } : {})
|
|
1853
1876
|
})).sort((a, b) => b.lastUsed - a.lastUsed)
|
|
1854
1877
|
},
|
|
1855
|
-
/**
|
|
1856
|
-
|
|
1878
|
+
/**
|
|
1879
|
+
* Retirar lo concedido a una aplicación. La próxima vez que pida, se vuelve a preguntar.
|
|
1880
|
+
* `onBehalfOf` es obligatorio para las que piden por otro (el puente): retirar el permiso
|
|
1881
|
+
* de UNA no puede quitárselo a todas las que entran por el mismo sitio.
|
|
1882
|
+
*/
|
|
1883
|
+
async revokeGrant ({ origin, onBehalfOf } = {}) {
|
|
1857
1884
|
const g = loadGrants()
|
|
1858
|
-
const
|
|
1859
|
-
if (!(
|
|
1860
|
-
delete g[
|
|
1885
|
+
const key = grantKey(String(origin || ''), behalfName(onBehalfOf))
|
|
1886
|
+
if (!(key in g)) return { ok: false }
|
|
1887
|
+
delete g[key]; saveGrants(g)
|
|
1861
1888
|
return { ok: true }
|
|
1862
1889
|
},
|
|
1863
1890
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/vault@0.
|
|
1
|
+
Copia vendorizada de @dotrino/vault@0.65.0 (dotrino-vault/lib/src/{index,enroll,protocol}.js).
|
|
2
2
|
NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
|
|
3
3
|
index.js importa ./enroll.js y ./protocol.js (relativos, van en esta misma copia),
|
|
4
4
|
@dotrino/identity/{capabilities,acta} (= ../../{capabilities,acta}.js) y
|
|
@@ -349,23 +349,50 @@ export function createEnrollDesk ({
|
|
|
349
349
|
return { ok: true, deviceId: pend.deviceId, adopting: true }
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
-
const { cert } = await identity.signDelegation(pend.dpub, pend.scope, { ttlMs: pend.ttlMs, label: pend.label })
|
|
353
|
-
|
|
354
352
|
// Aprobar un emparejamiento ES admitir al dispositivo en el perfil: el cert es la
|
|
355
353
|
// credencial y el acta es la política, y no tiene sentido emitir una sin la otra.
|
|
356
354
|
// Las capacidades salen del scope que se pidió al emparejar (cert ∩ acta, §2.3).
|
|
357
|
-
|
|
355
|
+
//
|
|
356
|
+
// NADA DE REPLIEGUES (2026-09-17). Esto antes estaba envuelto en un `catch` que solo
|
|
357
|
+
// anotaba el error, y además se saltaba en silencio si la identidad no sabía admitir o si
|
|
358
|
+
// el scope no daba ninguna capacidad: el aparato recibía su certificado SIN estar en el
|
|
359
|
+
// acta, y el fallo aparecía después y en otro sitio. Ahora, si no se puede admitir, no
|
|
360
|
+
// se entrega nada y se dice.
|
|
361
|
+
if (typeof identity.admitMember !== 'function') {
|
|
362
|
+
throw Object.assign(new Error('this vault cannot add devices to the account record: no certificate was issued'), { code: 'admit-unavailable' })
|
|
363
|
+
}
|
|
364
|
+
// PERMISOS, no tipos (2026-08-22): las capacidades son las del scope ENTERO. Un
|
|
365
|
+
// cajón (`secrets:<ns>`) suma `secrets` y fija el CN; no borra lo demás — un bot
|
|
366
|
+
// con `sign,secrets:eco` firma como aparato del acta Y lee solo su cajón.
|
|
367
|
+
const cn = scopeToCn(pend.scope)
|
|
368
|
+
const caps = [...new Set([...scopeToCaps(pend.scope), ...(cn ? ['secrets'] : [])])]
|
|
369
|
+
if (!caps.length) {
|
|
370
|
+
throw Object.assign(new Error('the pairing scope grants no permission: no certificate was issued'), { code: 'empty-scope' })
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const { cert } = await identity.signDelegation(pend.dpub, pend.scope, { ttlMs: pend.ttlMs, label: pend.label })
|
|
374
|
+
|
|
358
375
|
try {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
376
|
+
await identity.admitMember({ pub: pend.dpub, encPub: pend.encPub || null, label: pend.label || '', cn, caps, cert, continuity: pend.continuity || null })
|
|
377
|
+
} catch (e) {
|
|
378
|
+
// El certificado ya está firmado y no se entrega: se revoca, para que no quede uno
|
|
379
|
+
// válido suelto. Si ni eso sale, se dice también.
|
|
380
|
+
let revoked = true
|
|
381
|
+
try { await identity.revokeDelegation(cert.nonce) } catch (re) {
|
|
382
|
+
revoked = false
|
|
383
|
+
log('[vault] could not revoke the undelivered certificate %s: %s', cert.nonce, re.message)
|
|
366
384
|
}
|
|
367
|
-
|
|
368
|
-
|
|
385
|
+
audit('rejected', { what: 'approve', device: pend.deviceId, reason: 'admit-failed' })
|
|
386
|
+
reply(pend.from, { type: MSG_ERROR, error: 'the vault could not add this device to the account: pairing failed, try again' })
|
|
387
|
+
pending.delete(pend.token)
|
|
388
|
+
fire(onPendingChange)
|
|
389
|
+
log('[vault] could not add %s to the record, no certificate was delivered: %s', pend.deviceId, e.message)
|
|
390
|
+
throw Object.assign(
|
|
391
|
+
new Error(`could not add the device to the account record (${e.message}): no certificate was delivered${revoked ? '' : ', and the signed one could not be revoked'}`),
|
|
392
|
+
{ code: 'admit-failed', cause: e }
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
const record = (await identity.profileActa?.())?.acta || null
|
|
369
396
|
|
|
370
397
|
audit('enroll', { device: pend.deviceId, label: pend.label || '', scope: pend.scope })
|
|
371
398
|
// Echamos el código tipeado junto al cert: el DISPOSITIVO acepta solo si coincide
|