@dotrino/identity 0.104.2 → 0.105.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 +2 -0
- package/src/node.js +1 -0
- package/vault/core.js +25 -17
- package/vault/vendor/vault/VERSION.txt +1 -1
- package/vault/vendor/vault/enroll.js +23 -5
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -348,6 +348,8 @@ export class Identity {
|
|
|
348
348
|
|
|
349
349
|
/** Cambia las capacidades de un miembro (solo el master). */
|
|
350
350
|
async setCaps (pub, caps) { return this._call('setCaps', { pub, caps }) }
|
|
351
|
+
/** Cambia las capacidades de VARIOS miembros en una sola acta: `[{ pub, caps }]`. */
|
|
352
|
+
async setCapsMany (changes) { return this._call('setCapsMany', { changes }) }
|
|
351
353
|
async setLabel (pub, label) { return this._call('setLabel', { pub, label }) }
|
|
352
354
|
|
|
353
355
|
/** Expulsa a un miembro (solo el master; al master no se le puede expulsar). */
|
package/src/node.js
CHANGED
|
@@ -228,6 +228,7 @@ export class Identity {
|
|
|
228
228
|
/** Registra la llave de cifrado de un miembro ya admitido (evita re-enrolarlo). */
|
|
229
229
|
setMemberEncPub (args) { return this._h('setMemberEncPub', args) }
|
|
230
230
|
setCaps (pub, caps) { return this._h('setCaps', { pub, caps }) }
|
|
231
|
+
setCapsMany (changes) { return this._h('setCapsMany', { changes }) }
|
|
231
232
|
setLabel (pub, label) { return this._h('setLabel', { pub, label }) }
|
|
232
233
|
removeMember (pub) { return this._h('removeMember', { pub }) }
|
|
233
234
|
handoverMaster (to, member = null) { return this._h('handoverMaster', { to, member }) }
|
package/vault/core.js
CHANGED
|
@@ -973,19 +973,6 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
973
973
|
keyring: loadActa()?.keyring, myPub: publickeyJwkStr, myEncPrivateKey: encKeypair.privateKey
|
|
974
974
|
})
|
|
975
975
|
|
|
976
|
-
/** Envuelve la clave vigente para un miembro recién admitido (no hace falta rotar). */
|
|
977
|
-
async function wrapForNewMember (pub) {
|
|
978
|
-
const acta = loadActa()
|
|
979
|
-
const gen = (acta?.keyring || []).at(-1)
|
|
980
|
-
const m = acta?.members.find((x) => x.pub === pub)
|
|
981
|
-
if (!gen || !m?.encPub) return false
|
|
982
|
-
const mine = await myCek()
|
|
983
|
-
if (!mine) return false
|
|
984
|
-
const wrap = await Content.wrapForMember({ cek: mine.cek, memberEncPub: m.encPub })
|
|
985
|
-
await sealChanges([{ op: 'wrap', gen: mine.gen, pub, wrap }])
|
|
986
|
-
return true
|
|
987
|
-
}
|
|
988
|
-
|
|
989
976
|
/**
|
|
990
977
|
* QUITAR UN APARATO, entero y de una vez.
|
|
991
978
|
*
|
|
@@ -2571,12 +2558,21 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
2571
2558
|
// `cn`: si viene, este miembro es un SERVICIO y su único permiso es abrir el cajón de
|
|
2572
2559
|
// secretos de ESE nombre — no ve nada más del usuario. Sin `cn` es un dispositivo.
|
|
2573
2560
|
const finales = caps || (cn ? ['secrets'] : ['store', 'read'])
|
|
2574
|
-
const acta = await sealChanges([{ op: 'admit', member: { pub, encPub, label, cn, caps: finales, cert, continuity } }])
|
|
2575
2561
|
// Que entre al perfil incluye poder LEER lo que ya hay: se le envuelve la clave
|
|
2576
2562
|
// vigente (no hace falta rotar; rotar es para cuando alguien SALE).
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2563
|
+
//
|
|
2564
|
+
// EN LA MISMA ACTA que la admisión (dueño, 2026-09-26: nada de actas inútiles). Antes
|
|
2565
|
+
// se sellaba una para admitir y otra para envolver, y cada acta es un aviso a todos
|
|
2566
|
+
// los aparatos. La envoltura solo necesita la llave de cifrado del que entra, que ya
|
|
2567
|
+
// viene aquí. Si no se puede envolver (sin clave propia, sin `encPub`), entra igual y
|
|
2568
|
+
// `wrapped` lo dice — lo mismo que antes.
|
|
2569
|
+
let wrap = null
|
|
2570
|
+
try {
|
|
2571
|
+
const mine = encPub ? await myCek() : null
|
|
2572
|
+
if (mine) wrap = { op: 'wrap', gen: mine.gen, pub, wrap: await Content.wrapForMember({ cek: mine.cek, memberEncPub: encPub }) }
|
|
2573
|
+
} catch (_) { wrap = null }
|
|
2574
|
+
const acta = await sealChanges([{ op: 'admit', member: { pub, encPub, label, cn, caps: finales, cert, continuity } }, ...(wrap ? [wrap] : [])])
|
|
2575
|
+
return { ok: true, seq: acta.seq, wrapped: !!wrap }
|
|
2580
2576
|
},
|
|
2581
2577
|
|
|
2582
2578
|
async setCaps ({ pub, caps } = {}) {
|
|
@@ -2584,6 +2580,18 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
2584
2580
|
return { ok: true, seq: acta.seq }
|
|
2585
2581
|
},
|
|
2586
2582
|
|
|
2583
|
+
/**
|
|
2584
|
+
* LOS PERMISOS DE VARIOS APARATOS EN UNA SOLA ACTA (dueño, 2026-09-26). Editar tres
|
|
2585
|
+
* aparatos eran tres actas y tres avisos; ahora el borrador entero se sella de golpe.
|
|
2586
|
+
* `changes`: `[{ pub, caps }]`, cada `caps` la lista COMPLETA de ese aparato.
|
|
2587
|
+
* @param {{ changes?: Array<{ pub: string, caps: string[] }> }} [args]
|
|
2588
|
+
*/
|
|
2589
|
+
async setCapsMany ({ changes } = {}) {
|
|
2590
|
+
if (!Array.isArray(changes) || !changes.length) throw new Error('setCapsMany: no changes')
|
|
2591
|
+
const acta = await sealChanges(changes.map(({ pub, caps }) => ({ op: 'caps', pub, caps })))
|
|
2592
|
+
return { ok: true, seq: acta.seq }
|
|
2593
|
+
},
|
|
2594
|
+
|
|
2587
2595
|
/**
|
|
2588
2596
|
* RENOMBRA un miembro (el nombre con el que lo reconoces). Se escribe en el acta y
|
|
2589
2597
|
* TAMBIÉN en la delegación: son dos registros distintos —el acta dice quién es del
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/vault@0.
|
|
1
|
+
Copia vendorizada de @dotrino/vault@0.74.0 (dotrino-vault/lib/src/{index,enroll,protocol,passwordLogins,loginClient,b64}.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
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* `docs/pairing-protocol.md`).
|
|
31
31
|
*/
|
|
32
32
|
import { verifyDeviceSig, pubkeyId, commitCode } from '@dotrino/identity/capabilities'
|
|
33
|
-
import { verifyContinuity, canSeal } from '@dotrino/identity/acta'
|
|
33
|
+
import { verifyContinuity, canSeal, DEVICE_CAPS } from '@dotrino/identity/acta'
|
|
34
34
|
|
|
35
35
|
/** Un token de emparejamiento vale 5 min. */
|
|
36
36
|
export const PAIRING_TTL_MS = 5 * 60 * 1000
|
|
@@ -159,8 +159,24 @@ export function createEnrollDesk ({
|
|
|
159
159
|
* · `account` → cómo se llama la cuenta de la bóveda, para nombrarla en el
|
|
160
160
|
* aviso. Es ORIENTATIVO (un nombre que puso su dueño); la
|
|
161
161
|
* identidad de verdad de la cuenta es `iss`.
|
|
162
|
+
* · `caps` → LOS PERMISOS con los que entra al acta, elegidos por quien
|
|
163
|
+
* empareja (dueño, 2026-09-26: «al emparejar debería escoger los
|
|
164
|
+
* permisos, caso contrario estoy obligado a hacer dos actas»). Sin
|
|
165
|
+
* esto el aparato entraba con lo que da el scope y cualquier otro
|
|
166
|
+
* permiso —`admin`, `approve`…— costaba una segunda acta. Se quedan
|
|
167
|
+
* AQUÍ, en la sesión de la bóveda: NO viajan en el QR, así que la
|
|
168
|
+
* invitación que circula no concede nada; lo concede quien teclea
|
|
169
|
+
* el código, igual que hacía `caps` después.
|
|
162
170
|
*/
|
|
163
|
-
async function startPairing ({ scope = defaultScope, ttlMs = defaultTtlMs, label = '', mode = 'join', account = '' } = {}) {
|
|
171
|
+
async function startPairing ({ scope = defaultScope, ttlMs = defaultTtlMs, label = '', mode = 'join', account = '', caps = null } = {}) {
|
|
172
|
+
if (caps != null) {
|
|
173
|
+
if (!Array.isArray(caps)) throw Object.assign(new Error('caps must be a list'), { code: 'bad-caps' })
|
|
174
|
+
const cn = scopeToCn(scope)
|
|
175
|
+
const bad = caps.filter((c) => !(DEVICE_CAPS.includes(c) || (c === 'secrets' && cn)))
|
|
176
|
+
if (bad.length) throw Object.assign(new Error('unknown permission(s): ' + bad.join(', ')), { code: 'bad-caps' })
|
|
177
|
+
if (!caps.length) throw Object.assign(new Error('the pairing grants no permission'), { code: 'empty-scope' })
|
|
178
|
+
caps = [...new Set(caps)]
|
|
179
|
+
}
|
|
164
180
|
pending.clear() // uno a la vez: una sesión nueva supersede a la anterior
|
|
165
181
|
const acct = String(account || '').slice(0, 40)
|
|
166
182
|
// INVITACIÓN CORTA: si sabemos cómo alcanzarnos, el QR lleva solo eso y el
|
|
@@ -176,12 +192,12 @@ export function createEnrollDesk ({
|
|
|
176
192
|
const conn = typeof connToken === 'function' ? await connToken() : connToken
|
|
177
193
|
if (conn) {
|
|
178
194
|
const sn = randToken(8)
|
|
179
|
-
pending.set(sn, { token: sn, exp: Date.now() + PAIRING_TTL_MS, scope, ttlMs, label, sn, mode, account: acct, state: 'AWAITING_ENROLL' })
|
|
195
|
+
pending.set(sn, { token: sn, exp: Date.now() + PAIRING_TTL_MS, scope, caps, ttlMs, label, sn, mode, account: acct, state: 'AWAITING_ENROLL' })
|
|
180
196
|
return { token: sn, qr: { v: 2, conn, sn, m: mode, proxy }, expiresInMs: PAIRING_TTL_MS }
|
|
181
197
|
}
|
|
182
198
|
const token = randToken(PAIR_TOKEN_BYTES)
|
|
183
199
|
const sn = randToken(PAIR_TOKEN_BYTES)
|
|
184
|
-
pending.set(token, { token, exp: Date.now() + PAIRING_TTL_MS, scope, ttlMs, label, sn, mode, account: acct, state: 'AWAITING_ENROLL' })
|
|
200
|
+
pending.set(token, { token, exp: Date.now() + PAIRING_TTL_MS, scope, caps, ttlMs, label, sn, mode, account: acct, state: 'AWAITING_ENROLL' })
|
|
185
201
|
return { token, qr: { v: 2, iss, proxy, token, sn, m: mode, ...(acct ? { acct } : {}) }, expiresInMs: PAIRING_TTL_MS }
|
|
186
202
|
}
|
|
187
203
|
|
|
@@ -364,8 +380,10 @@ export function createEnrollDesk ({
|
|
|
364
380
|
// PERMISOS, no tipos (2026-08-22): las capacidades son las del scope ENTERO. Un
|
|
365
381
|
// cajón (`secrets:<ns>`) suma `secrets` y fija el CN; no borra lo demás — un bot
|
|
366
382
|
// con `sign,secrets:eco` firma como aparato del acta Y lee solo su cajón.
|
|
383
|
+
// Si quien emparejó ELIGIÓ los permisos (`startPairing({ caps })`), entran esos y en
|
|
384
|
+
// esta misma acta; si no, los que da el scope.
|
|
367
385
|
const cn = scopeToCn(pend.scope)
|
|
368
|
-
const caps = [...new Set([...scopeToCaps(pend.scope), ...(cn ? ['secrets'] : [])])]
|
|
386
|
+
const caps = [...new Set([...(pend.caps || scopeToCaps(pend.scope)), ...(cn ? ['secrets'] : [])])]
|
|
369
387
|
if (!caps.length) {
|
|
370
388
|
throw Object.assign(new Error('the pairing scope grants no permission: no certificate was issued'), { code: 'empty-scope' })
|
|
371
389
|
}
|