@dotrino/vault 0.73.0 → 0.74.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/enroll.js +23 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vault",
3
- "version": "0.73.0",
3
+ "version": "0.74.0",
4
4
  "description": "Usa ESTE dispositivo (navegador) como bóveda/CA del ecosistema Dotrino: atiende enrolamientos por el proxy y firma certificados de delegación a tus máquinas. Incluye el cliente de SERVICIO (Node): un proyecto se enrola una vez y jala sus credenciales del vault en vez del .env (`import '@dotrino/vault/config'`).",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/enroll.js CHANGED
@@ -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
  }