@dotrino/vaultd 0.124.2 → 0.125.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.
@@ -26,8 +26,8 @@
26
26
  */
27
27
  import { server as opaquePorDefecto, suiteId as suitePorDefecto } from '@dotrino/opaque'
28
28
  import { pubkeyId } from '@dotrino/identity/capabilities'
29
- import { deviceIdOf, scopeToCaps, scopeToCn } from './enroll.js'
30
- import { SCOPE } from './protocol.js'
29
+ import { DEVICE_CAPS, capScope } from '@dotrino/identity/acta'
30
+ import { deviceIdOf } from './enroll.js'
31
31
  import { bytesToB64url, b64urlToBytes } from './b64.js'
32
32
 
33
33
  /** Un usuario es la parte de antes de la `@` en `nombre@AB12-CD34-EF56`. */
@@ -445,24 +445,35 @@ export function createLoginDesk ({ load, save, now = () => Date.now(), opaque =
445
445
  * @param {object} opts
446
446
  * `identity` la identidad que firma (`signDelegation`, `admitMember`)
447
447
  * `logins` el escritorio de `createLoginDesk`
448
- * `scope` permisos del aparato; por defecto firmar, leer y guardar
449
- * `unattended` si además puede trabajar sin que nadie apruebe
448
+ * `caps` los PERMISOS del acta que lleva el aparato, cualquiera de `DEVICE_CAPS`;
449
+ * por defecto firmar, leer y guardar
450
450
  */
451
451
  export async function registerLogin ({
452
452
  identity, logins, user, upload, pub, encPub = null, label = '', blob,
453
- scope, unattended = false, replace = false
453
+ caps = ['sign', 'read', 'store'], scope, replace = false
454
454
  } = {}) {
455
455
  if (replace) {
456
456
  await logins.registerFinish({ user, upload, blob, replace: true })
457
457
  return { ok: true, user, replaced: true }
458
458
  }
459
- // PERMISOS, no tipos: los del scope, más `unattended` si quien lo crea lo eligió
460
- // (temporary-access.md §3.1). `passwords` no entra todavía — hasta que existan las
461
- // contraseñas selladas, este aparato se llevaría TODAS (sealed-passwords.md).
462
- const scopes = Array.isArray(scope) && scope.length ? scope : [SCOPE.SIGN, SCOPE.READ, SCOPE.STORE]
463
- if (scopes.includes(SCOPE.PASSWORDS)) {
464
- throw Object.assign(new Error('a password login cannot take `contrasenas` yet: the sealed passwords come first'), { code: 'passwords-not-yet' })
459
+ // CUALQUIER PERMISO, y todos por el mismo camino (dueño, 2026-09-21: «al crear debe
460
+ // poder asignársele cualquier permiso»; regla dura «simplificar los procesos»). Antes
461
+ // llegaban como scopes del certificado con `unattended` aparte, `passwords` se rechazaba
462
+ // con `passwords-not-yet` y `passkeys` —que no tiene scope— no se podía asignar.
463
+ //
464
+ // `scope` ya no se acepta, y se dice: ignorarlo daría al aparato los permisos por defecto
465
+ // en vez de los que se eligieron, y nadie lo notaría.
466
+ if (scope !== undefined) {
467
+ throw Object.assign(new Error('registerLogin takes `caps` (account record permissions), not `scope`'), { code: 'use-caps' })
465
468
  }
469
+ const pedidos = [...new Set(Array.isArray(caps) ? caps : [])]
470
+ const raros = pedidos.filter((c) => !DEVICE_CAPS.includes(c))
471
+ if (!pedidos.length || raros.length) {
472
+ throw Object.assign(new Error(`not device permissions: ${raros.join(', ') || '(none)'} — expected any of ${DEVICE_CAPS.join(', ')}`), { code: 'bad-caps' })
473
+ }
474
+ // El certificado lleva el scope de los permisos que lo tienen; los demás (`unattended`,
475
+ // `passkeys`) viven solo en el acta, que es quien decide.
476
+ const scopes = pedidos.map((c) => capScope(c)).filter(Boolean)
466
477
  if (typeof identity?.admitMember !== 'function') {
467
478
  throw Object.assign(new Error('this vault cannot add devices to the account record: no login was created'), { code: 'admit-unavailable' })
468
479
  }
@@ -470,11 +481,9 @@ export async function registerLogin ({
470
481
  await logins.registerFinish({ user, upload, pub, encPub, deviceId, label, blob })
471
482
  try {
472
483
  const { cert } = await identity.signDelegation(pub, scopes, { label: label || `login:${user}` })
473
- const cn = scopeToCn(scopes)
474
- const caps = [...new Set([...scopeToCaps(scopes), ...(unattended ? ['unattended'] : [])])]
475
- await identity.admitMember({ pub, encPub, label: label || `login:${user}`, cn, caps, cert })
484
+ await identity.admitMember({ pub, encPub, label: label || `login:${user}`, cn: null, caps: pedidos, cert })
476
485
  logins.setCert({ user, cert, iss: identity.me?.publickey || null })
477
- return { ok: true, user, deviceId, cert, caps }
486
+ return { ok: true, user, deviceId, cert, caps: pedidos }
478
487
  } catch (e) {
479
488
  // NADA DE MEDIAS ALTAS: si no entra en el acta, no queda un usuario que pueda entrar a
480
489
  // una cuenta que no lo reconoce. Se deshace y se dice.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vaultd",
3
- "version": "0.124.2",
3
+ "version": "0.125.0",
4
4
  "type": "module",
5
5
  "description": "Certificador personal de Dotrino: daemon headless que custodia la clave maestra y delega capacidades a tus dispositivos por el proxy. Tu CA propia.",
6
6
  "bin": {
@@ -24,7 +24,7 @@
24
24
  "@dotrino/compat": "0.4.0",
25
25
  "@dotrino/identity": "0.92.0",
26
26
  "@dotrino/opaque": "0.1.0",
27
- "@dotrino/passmanager": "0.16.0",
27
+ "@dotrino/passmanager": "0.17.0",
28
28
  "@dotrino/proxy-client": "0.23.1",
29
29
  "@dotrino/roadmap": "0.3.3",
30
30
  "@dotrino/store": "0.11.0",
package/src/ctl.js CHANGED
@@ -1620,7 +1620,18 @@ async function cmdLogins (rest) {
1620
1620
  }
1621
1621
  case 'add': {
1622
1622
  needUser()
1623
- const label = args.slice(1).join(' ').trim() || 'equipo prestado'
1623
+ // `±permiso` se lee con las MISMAS palabras que `caps <ID> ±permiso`, sobre los de
1624
+ // siempre (firma, lee, guarda); lo demás es el nombre del equipo. Antes el texto de
1625
+ // ayuda lo anunciaba y aquí se juntaba todo como nombre.
1626
+ const caps = new Set(['sign', 'read', 'store'])
1627
+ const nombre = []
1628
+ for (const a of args.slice(1)) {
1629
+ if (a[0] !== '+' && a[0] !== '-') { nombre.push(a); continue }
1630
+ const cap = CAP_BY_WORD[a.slice(1).toLowerCase()]
1631
+ if (!cap) { console.error('permiso no reconocido: %s', a); process.exit(2) }
1632
+ if (a[0] === '+') caps.add(cap); else caps.delete(cap)
1633
+ }
1634
+ const label = nombre.join(' ').trim() || 'equipo prestado'
1624
1635
  const { client: opaqueClient } = await import('@dotrino/opaque')
1625
1636
  const { makeDeviceKey, makeDeviceEncKey } = await import('@dotrino/identity/capabilities')
1626
1637
  const { sealDeviceKeys } = await import('../lib/src/passwordLogins.js')
@@ -1635,7 +1646,7 @@ async function cmdLogins (rest) {
1635
1646
  const enc = await makeDeviceEncKey()
1636
1647
  const blob = await sealDeviceKeys(fin.exportKey, { sign: device.privateJwk, enc: enc.privateJwk })
1637
1648
  const r = await loginsRequest('register-finish', {
1638
- user, upload: fin.upload, pub: device.publickey, encPub: enc.publickey, label, blob
1649
+ user, upload: fin.upload, pub: device.publickey, encPub: enc.publickey, label, blob, caps: [...caps]
1639
1650
  })
1640
1651
  const { loginAddress } = await import('../lib/src/passwordLogins.js')
1641
1652
  console.log('\nListo. Se entra con esta dirección y esa contraseña:')
package/src/daemon.js CHANGED
@@ -402,7 +402,7 @@ export async function runDaemon () {
402
402
  case 'register-begin': return vault.loginRegisterBegin({ user: req.user, request: req.request, replace: !!req.replace })
403
403
  case 'register-finish': return vault.loginRegisterFinish({
404
404
  user: req.user, upload: req.upload, pub: req.pub, encPub: req.encPub || null,
405
- label: req.label || '', blob: req.blob, scope: req.scope, unattended: !!req.unattended, replace: !!req.replace
405
+ label: req.label || '', blob: req.blob, ...(req.caps ? { caps: req.caps } : {}), replace: !!req.replace
406
406
  })
407
407
  // Entrar desde la propia máquina de la bóveda. Lo usa `logins passwd`: para volver a
408
408
  // cerrar el paquete de llaves hay que ABRIRLO antes, y eso solo puede hacerlo quien
@@ -291,7 +291,6 @@ export async function listLogins (profile) {
291
291
  export async function addLogin ({ user, password, label = 'equipo prestado', caps = ['sign', 'read', 'store'] } = {}) {
292
292
  const { client: opaque } = await import('@dotrino/opaque')
293
293
  const { makeDeviceKey, makeDeviceEncKey } = await import('@dotrino/identity/capabilities')
294
- const { capScope } = await import('@dotrino/identity/acta')
295
294
  const { sealDeviceKeys } = await import('../lib/src/passwordLogins.js')
296
295
  const start = opaque.registrationStart({ password })
297
296
  const begun = await loginsOp('register-begin', { user, request: start.request })
@@ -299,12 +298,10 @@ export async function addLogin ({ user, password, label = 'equipo prestado', cap
299
298
  const device = await makeDeviceKey({ label })
300
299
  const enc = await makeDeviceEncKey()
301
300
  const blob = await sealDeviceKeys(fin.exportKey, { sign: device.privateJwk, enc: enc.privateJwk })
302
- // El acta habla de PERMISOS y el papel de SCOPES: los traduce el pilar, no esta pantalla.
303
- // `unattended` no es un scope —es «no me pidas aprobación»— y por eso viaja aparte.
301
+ // Los PERMISOS del acta, tal cual: cualquiera, y todos por el mismo camino. Traducirlos a
302
+ // los scopes del certificado es cosa del pilar (`registerLogin`), no de esta pantalla.
304
303
  return loginsOp('register-finish', {
305
- user, upload: fin.upload, pub: device.publickey, encPub: enc.publickey, label, blob,
306
- scope: caps.filter((c) => c !== 'unattended').map((c) => capScope(c)).filter(Boolean),
307
- unattended: caps.includes('unattended')
304
+ user, upload: fin.upload, pub: device.publickey, encPub: enc.publickey, label, blob, caps
308
305
  })
309
306
  }
310
307