@dotrino/identity 0.98.0 → 0.100.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/identity",
3
- "version": "0.98.0",
3
+ "version": "0.100.0",
4
4
  "description": "Identidad y rating de usuarios compartidos entre apps de Dotrino (vault iframe + postMessage)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/vault/vault.js CHANGED
@@ -405,7 +405,7 @@ import { pubkeyId } from './capabilities.js'
405
405
  * la bóveda guarda un paquete que no puede abrir. Es el mismo camino que `logins add`
406
406
  * del binario, con la misma pieza compartida.
407
407
  */
408
- selfVaultLoginAdd: async ({ user, password, label = '', scope = null, unattended = false } = {}) => {
408
+ selfVaultLoginAdd: async ({ user, password, label = '', caps } = {}) => {
409
409
  const d = pidaDaemon()
410
410
  const { client: opaque } = await import('@dotrino/opaque')
411
411
  const { makeDeviceKey, makeDeviceEncKey } = await import('@dotrino/identity/capabilities')
@@ -415,14 +415,15 @@ import { pubkeyId } from './capabilities.js'
415
415
  }
416
416
  const nombre = String(label || 'equipo prestado')
417
417
  const reg = opaque.registrationStart({ password })
418
- const { response } = d.loginRegisterBegin({ user, request: reg.request })
418
+ const { response } = await d.loginRegisterBegin({ user, request: reg.request })
419
419
  const fin = opaque.registrationFinish({ state: reg.state, response, password })
420
420
  const device = await makeDeviceKey({ label: nombre })
421
421
  const enc = await makeDeviceEncKey()
422
422
  const blob = await sealDeviceKeys(fin.exportKey, { sign: device.privateJwk, enc: enc.encPrivateJwk })
423
423
  const r = await d.loginRegisterFinish({
424
424
  user, upload: fin.upload, pub: device.publickey, encPub: enc.encPublickey,
425
- label: nombre, blob, ...(Array.isArray(scope) && scope.length ? { scope } : {}), unattended: !!unattended
425
+ // Los PERMISOS del acta, cualquiera de ellos: los traduce a scopes el pilar del vault.
426
+ label: nombre, blob, ...(caps ? { caps } : {})
426
427
  })
427
428
  return { ...r, address: loginAddress(user, await accountFingerprint(selfIdentity)) }
428
429
  },
@@ -440,15 +441,15 @@ import { pubkeyId } from './capabilities.js'
440
441
  throw Object.assign(new Error('the password must be at least 12 characters'), { code: 'weak-password' })
441
442
  }
442
443
  const start = opaque.loginStart({ password: oldPassword })
443
- const begun = d.loginBegin({ user, request: start.request })
444
+ const begun = await d.loginBegin({ user, request: start.request })
444
445
  let fin
445
446
  try { fin = opaque.loginFinish({ state: start.state, response: begun.response, password: oldPassword }) }
446
447
  catch (_) { throw Object.assign(new Error('wrong password'), { code: 'login-failed' }) }
447
- const entered = d.loginEnd({ lid: begun.lid, finalization: fin.finalization, label: 'consola' })
448
+ const entered = await d.loginEnd({ lid: begun.lid, finalization: fin.finalization, label: 'consola' })
448
449
  const keys = await openDeviceKeys(fin.exportKey, entered.blob)
449
450
 
450
451
  const reg = opaque.registrationStart({ password: newPassword })
451
- const { response } = d.loginRegisterBegin({ user, request: reg.request, replace: true })
452
+ const { response } = await d.loginRegisterBegin({ user, request: reg.request, replace: true })
452
453
  const nueva = opaque.registrationFinish({ state: reg.state, response, password: newPassword })
453
454
  await d.loginRegisterFinish({
454
455
  user, upload: nueva.upload, blob: await sealDeviceKeys(nueva.exportKey, keys), replace: true
@@ -1,4 +1,4 @@
1
- Copia vendorizada de @dotrino/vault@0.67.0 (dotrino-vault/lib/src/{index,enroll,protocol,passwordLogins,loginClient,b64}.js).
1
+ Copia vendorizada de @dotrino/vault@0.71.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
@@ -394,7 +394,7 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
394
394
  async function handleLoginStart (from, p) {
395
395
  if (!logins) return noLogins(from)
396
396
  try {
397
- const { lid, response } = logins.loginBegin({ user: p?.user, request: p?.request })
397
+ const { lid, response } = await logins.loginBegin({ user: p?.user, request: p?.request })
398
398
  send(from, { type: MSG.LOGIN_RESPONSE, lid, response })
399
399
  } catch (e) { loginError(from, e) }
400
400
  }
@@ -402,7 +402,7 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
402
402
  async function handleLoginFinish (from, p) {
403
403
  if (!logins) return noLogins(from)
404
404
  try {
405
- const r = logins.loginEnd({ lid: p?.lid, finalization: p?.finalization, label: p?.label })
405
+ const r = await logins.loginEnd({ lid: p?.lid, finalization: p?.finalization, label: p?.label })
406
406
  const acta = (await identity.profileActa?.().catch(() => null))?.acta || null
407
407
  send(from, { type: MSG.LOGIN_OK, sid: r.sid, blob: r.blob, cert: r.cert, iss: r.iss || iss, acta })
408
408
  } catch (e) { loginError(from, e) }
@@ -34,7 +34,7 @@
34
34
  * Si algo de eso falla, se PARA con un `code` propio. No hay repliegue: entrar «a medias»
35
35
  * en una cuenta es peor que no entrar.
36
36
  */
37
- import { client as opaque } from '@dotrino/opaque'
37
+ import { client as opaquePorDefecto } from '@dotrino/opaque'
38
38
  import { pubkeyId, verifyDeviceSig, signWithDevice } from '@dotrino/identity/capabilities'
39
39
  import { checkVaultReply } from '@dotrino/identity/acta'
40
40
  import { MSG } from './protocol.js'
@@ -82,11 +82,16 @@ const parseJson = (s) => { try { return JSON.parse(s) } catch (_) { return null
82
82
  * @param {string} opts.password
83
83
  * @param {string} [opts.label] de dónde se entra («el cyber de la esquina»): es lo que
84
84
  * el dueño va a leer en su consola para decidir si cerrarlo.
85
+ * @param {object} [opts.opaque] el OPAQUE a usar. Por defecto el de casa, que va en este
86
+ * mismo hilo. Se inyecta porque en una EXTENSIÓN el WASM vive en una página sandbox, al
87
+ * otro lado de un `postMessage`, y entonces sus métodos devuelven promesas — igual que en
88
+ * `createLoginDesk`, y por el mismo motivo. Aquí se espera siempre: esperar a algo que no
89
+ * es una promesa no cuesta nada, y dos firmas serían dos clientes.
85
90
  * @returns {Promise<{user:string,address:string,code:string,sid:string,cert:object,
86
91
  * iss:string,acta:object,vaultToken:string,publickey:string,encPublickey:(string|null),
87
92
  * keys:{sign:object,enc:(object|null)}}>}
88
93
  */
89
- export async function loginWithPassword ({ transport, address, password, label = '', timeoutMs = REPLY_TIMEOUT_MS } = {}) {
94
+ export async function loginWithPassword ({ transport, address, password, label = '', timeoutMs = REPLY_TIMEOUT_MS, opaque = opaquePorDefecto } = {}) {
90
95
  if (!transport || typeof transport.on !== 'function' || typeof transport.send !== 'function') {
91
96
  throw err('no-transport', 'loginWithPassword: a connected transport is required')
92
97
  }
@@ -104,7 +109,7 @@ export async function loginWithPassword ({ transport, address, password, label =
104
109
  let ultimo = null
105
110
  for (const token of tokens) {
106
111
  try {
107
- return await unIntento({ transport, token, user, code, dir, password, label, timeoutMs })
112
+ return await unIntento({ transport, token, user, code, dir, password, label, timeoutMs, opaque })
108
113
  } catch (e) {
109
114
  // Se para: son cosas del que entra, y probar con otra bóveda no las arregla.
110
115
  if (e?.code === 'login-failed' || e?.code === 'too-many-tries' || e?.code === 'wrong-account' ||
@@ -115,15 +120,15 @@ export async function loginWithPassword ({ transport, address, password, label =
115
120
  throw ultimo || err('no-vault', 'none of the vaults on that address could let you in')
116
121
  }
117
122
 
118
- async function unIntento ({ transport, token, user, code, dir, password, label, timeoutMs }) {
119
- const start = opaque.loginStart({ password })
123
+ async function unIntento ({ transport, token, user, code, dir, password, label, timeoutMs, opaque }) {
124
+ const start = await opaque.loginStart({ password })
120
125
  transport.send(token, { type: MSG.LOGIN_START, user, request: start.request })
121
126
  const r1 = await waitFor(transport, token, (p) => p.type === MSG.LOGIN_RESPONSE || p.type === MSG.ERROR, timeoutMs)
122
127
  if (r1.type === MSG.ERROR) throw deVuelta(r1)
123
128
 
124
129
  let fin
125
130
  try {
126
- fin = opaque.loginFinish({ state: start.state, response: r1.response, password })
131
+ fin = await opaque.loginFinish({ state: start.state, response: r1.response, password })
127
132
  } catch (_) {
128
133
  // OPAQUE comprueba a las dos partes: aquí se sabe que el otro lado NO conoce el
129
134
  // registro de este usuario. Contraseña equivocada, usuario que no existe o una bóveda
@@ -198,11 +203,26 @@ function deVuelta (p) {
198
203
  * quedarse bloqueado esperándola. La sesión sigue abierta allí hasta que se cierre desde la
199
204
  * consola, y eso ya está dicho en el diseño (§3.2).
200
205
  */
201
- export async function closeLogin ({ transport, token, user, sid, publickey, privateJwk, privateKey, timeoutMs = REPLY_TIMEOUT_MS } = {}) {
206
+ export async function closeLogin ({ transport, token, user, sid, publickey, privateJwk, privateKey, sign, timeoutMs = REPLY_TIMEOUT_MS } = {}) {
202
207
  if (!transport || typeof transport.send !== 'function') throw err('no-transport', 'closeLogin: transport required')
203
208
  if (!token || !user || !sid || !publickey) throw err('bad-input', 'closeLogin: token, user, sid and publickey are required')
204
209
  const data = { op: 'login.close', publickey, user, sid, ts: Date.now() }
205
- const { signature } = await signWithDevice({ privateJwk, privateKey, publickey, data })
210
+ // CON LA LLAVE O CON QUIEN LA TIENE. `sign(data)` es lo normal en el ecosistema —el
211
+ // transporte se identifica con `identity.signData` y no con una llave suelta— y aquí hace
212
+ // falta de verdad: en una extensión la llave vive en el service worker y quien tiene el
213
+ // socket es la página, así que o se firma a distancia o la llave privada tendría que
214
+ // cruzar para nada. Sin ninguna de las dos no se firma, y salir no se puede fingir.
215
+ //
216
+ // Se comprueba ANTES de firmar: sin nada con qué hacerlo, WebCrypto revienta con un error
217
+ // suyo que no dice qué falta («code: 0»), y el de arriba es el que se puede buscar.
218
+ if (typeof sign !== 'function' && !privateJwk && !privateKey) {
219
+ throw err('no-signature', 'closeLogin: give it the device key (privateJwk/privateKey) or a sign(data)')
220
+ }
221
+ const firmado = typeof sign === 'function'
222
+ ? await sign(data)
223
+ : await signWithDevice({ privateJwk, privateKey, publickey, data })
224
+ const signature = typeof firmado === 'string' ? firmado : firmado?.signature
225
+ if (typeof signature !== 'string') throw err('no-signature', 'closeLogin: sign(data) returned no signature')
206
226
  transport.send(token, { type: MSG.LOGIN_CLOSE, data, signature })
207
227
  try {
208
228
  const r = await waitFor(transport, token, (p) => p.type === MSG.LOGIN_CLOSED || p.type === MSG.ERROR, timeoutMs)
@@ -24,10 +24,10 @@
24
24
  * binario, `lib/src/index.js` en la pestaña—. Lo único que no es puro es `registerLogin`, al
25
25
  * final, porque hace falta la identidad que firma.
26
26
  */
27
- import { server as opaque, suiteId } from '@dotrino/opaque'
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`. */
@@ -181,10 +181,25 @@ export async function openDeviceKeys (exportKey, blob) {
181
181
  * `load()` devuelve el estado guardado (o `null` la primera vez)
182
182
  * `save(s)` lo guarda
183
183
  * `now()` el reloj, para poder probar el freno sin esperar una hora
184
+ * `opaque` la mitad de SERVIDOR de OPAQUE. Por defecto la del paquete, que es WASM y
185
+ * corre en el mismo hilo. Se puede inyectar OTRA porque en una extensión MV3 el WASM
186
+ * vive al otro lado de un `postMessage` —en una página sandbox, para no tener que
187
+ * abrirle la CSP a toda la extensión—, y desde allí solo puede contestar una promesa.
188
+ *
189
+ * **Por eso los métodos que tocan OPAQUE son ASÍNCRONOS**, también con la implementación de
190
+ * casa: una firma que cambia según quién la use son dos mostradores, y de eso va la regla
191
+ * de las tres versiones.
184
192
  */
185
- export function createLoginDesk ({ load, save, now = () => Date.now() } = {}) {
193
+ export function createLoginDesk ({ load, save, now = () => Date.now(), opaque = opaquePorDefecto } = {}) {
186
194
  if (typeof load !== 'function' || typeof save !== 'function') throw new Error('createLoginDesk: load and save are required')
187
195
 
196
+ /**
197
+ * QUÉ SUITE se está usando, dicha por quien hace el cálculo. Con el OPAQUE de casa es la
198
+ * del paquete; con uno inyectado, la suya — preguntárselo a él es lo único que no miente,
199
+ * porque el registro que queda guardado lo hizo él.
200
+ */
201
+ const suite = () => (typeof opaque.suiteId === 'function' ? opaque.suiteId() : suitePorDefecto())
202
+
188
203
  /** Intercambios a medias: viven en memoria, y por eso un reinicio los tira. */
189
204
  const exchanges = new Map()
190
205
 
@@ -195,9 +210,9 @@ export function createLoginDesk ({ load, save, now = () => Date.now() } = {}) {
195
210
  }
196
211
 
197
212
  /** La preparación del servidor nace con el primer usuario y no se toca nunca más. */
198
- function setupOf (state) {
213
+ async function setupOf (state) {
199
214
  if (state.setup) return state.setup
200
- state.setup = opaque.createSetup()
215
+ state.setup = await opaque.createSetup()
201
216
  return state.setup
202
217
  }
203
218
 
@@ -234,30 +249,30 @@ export function createLoginDesk ({ load, save, now = () => Date.now() } = {}) {
234
249
  * ALTA, primer paso. La contraseña no llega hasta aquí: lo que llega es el mensaje de
235
250
  * registro de OPAQUE, que no la lleva ni permite adivinarla.
236
251
  */
237
- registerBegin ({ user, request, replace = false } = {}) {
252
+ async registerBegin ({ user, request, replace = false } = {}) {
238
253
  const state = read()
239
254
  const existing = userOf(state, user)
240
255
  if (existing && !replace) throw err('user-exists', `there is already a login called "${user}"`)
241
256
  if (!existing && replace) throw err('no-user', `there is no login called "${user}"`)
242
257
  if (typeof request !== 'string' || !request) throw err('bad-input', 'request required')
243
- const setup = setupOf(state)
244
- const response = opaque.registrationResponse({ setup, request, credentialId: user })
258
+ const setup = await setupOf(state)
259
+ const response = await opaque.registrationResponse({ setup, request, credentialId: user })
245
260
  save(state)
246
- return { response, suite: suiteId() }
261
+ return { response, suite: await suite() }
247
262
  },
248
263
 
249
264
  /**
250
265
  * ALTA, segundo paso. `blob` son las llaves privadas del aparato, ya cerradas por quien
251
266
  * creó el aparato con la llave que sale de la contraseña: aquí no se abre nunca.
252
267
  */
253
- registerFinish ({ user, upload, pub, encPub = null, deviceId = null, label = '', blob, replace = false } = {}) {
268
+ async registerFinish ({ user, upload, pub, encPub = null, deviceId = null, label = '', blob, replace = false } = {}) {
254
269
  const state = read()
255
270
  const existing = userOf(state, user)
256
271
  if (existing && !replace) throw err('user-exists', `there is already a login called "${user}"`)
257
272
  if (!existing && replace) throw err('no-user', `there is no login called "${user}"`)
258
273
  if (typeof blob !== 'string' || !blob) throw err('bad-input', 'blob required (the device keys, sealed with the password)')
259
274
  if (!replace && (typeof pub !== 'string' || !pub)) throw err('bad-input', 'pub required')
260
- const record = opaque.registrationFinish({ upload })
275
+ const record = await opaque.registrationFinish({ upload })
261
276
  const t = now()
262
277
  state.users[user] = {
263
278
  ...(existing || {}),
@@ -267,7 +282,7 @@ export function createLoginDesk ({ load, save, now = () => Date.now() } = {}) {
267
282
  label: label || existing?.label || '',
268
283
  record,
269
284
  blob,
270
- suite: suiteId(),
285
+ suite: await suite(),
271
286
  createdAt: existing?.createdAt || t,
272
287
  passwordChangedAt: t,
273
288
  // Cambiar la contraseña cierra lo abierto: si alguien entró con la vieja, deja de
@@ -277,7 +292,7 @@ export function createLoginDesk ({ load, save, now = () => Date.now() } = {}) {
277
292
  nextTryAt: 0
278
293
  }
279
294
  save(state)
280
- return { user, suite: suiteId() }
295
+ return { user, suite: await suite() }
281
296
  },
282
297
 
283
298
  /** El certificado del aparato, que firma la bóveda al admitirlo en el acta. Es público. */
@@ -298,7 +313,7 @@ export function createLoginDesk ({ load, save, now = () => Date.now() } = {}) {
298
313
  * registro inventado: desde fuera no se puede averiguar qué usuarios hay. Y cuenta
299
314
  * contra el mismo freno, para que tampoco se pueda medir por el tiempo.
300
315
  */
301
- loginBegin ({ user, request } = {}) {
316
+ async loginBegin ({ user, request } = {}) {
302
317
  sweepExchanges()
303
318
  const state = read()
304
319
  if (typeof request !== 'string' || !request) throw err('bad-input', 'request required')
@@ -314,8 +329,8 @@ export function createLoginDesk ({ load, save, now = () => Date.now() } = {}) {
314
329
  u.fails = (u.fails || 0) + 1
315
330
  u.nextTryAt = t + backoffFor(u.fails)
316
331
  }
317
- const setup = setupOf(state)
318
- const { state: serverState, response } = opaque.loginStart({
332
+ const setup = await setupOf(state)
333
+ const { state: serverState, response } = await opaque.loginStart({
319
334
  setup,
320
335
  record: u?.record || null,
321
336
  request,
@@ -332,7 +347,7 @@ export function createLoginDesk ({ load, save, now = () => Date.now() } = {}) {
332
347
  * INICIO DE SESIÓN, segundo paso. Si la contraseña es la buena, devuelve el paquete de
333
348
  * llaves —que solo abre esa contraseña—, el certificado y el acta.
334
349
  */
335
- loginEnd ({ lid, finalization, label = '' } = {}) {
350
+ async loginEnd ({ lid, finalization, label = '' } = {}) {
336
351
  sweepExchanges()
337
352
  const x = exchanges.get(lid)
338
353
  if (!x) throw err('no-exchange', 'that login is not in flight any more: start again')
@@ -341,7 +356,7 @@ export function createLoginDesk ({ load, save, now = () => Date.now() } = {}) {
341
356
  const u = x.known ? state.users[x.user] : null
342
357
 
343
358
  try {
344
- opaque.loginFinish({ state: x.state, finalization })
359
+ await opaque.loginFinish({ state: x.state, finalization })
345
360
  } catch (e) {
346
361
  // Contraseña equivocada, usuario inexistente o mensaje alterado: el MISMO error.
347
362
  // Distinguirlos diría qué usuarios existen. El intento ya está contado desde
@@ -430,36 +445,45 @@ export function createLoginDesk ({ load, save, now = () => Date.now() } = {}) {
430
445
  * @param {object} opts
431
446
  * `identity` la identidad que firma (`signDelegation`, `admitMember`)
432
447
  * `logins` el escritorio de `createLoginDesk`
433
- * `scope` permisos del aparato; por defecto firmar, leer y guardar
434
- * `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
435
450
  */
436
451
  export async function registerLogin ({
437
452
  identity, logins, user, upload, pub, encPub = null, label = '', blob,
438
- scope, unattended = false, replace = false
453
+ caps = ['sign', 'read', 'store'], scope, replace = false
439
454
  } = {}) {
440
455
  if (replace) {
441
- logins.registerFinish({ user, upload, blob, replace: true })
456
+ await logins.registerFinish({ user, upload, blob, replace: true })
442
457
  return { ok: true, user, replaced: true }
443
458
  }
444
- // PERMISOS, no tipos: los del scope, más `unattended` si quien lo crea lo eligió
445
- // (temporary-access.md §3.1). `passwords` no entra todavía — hasta que existan las
446
- // contraseñas selladas, este aparato se llevaría TODAS (sealed-passwords.md).
447
- const scopes = Array.isArray(scope) && scope.length ? scope : [SCOPE.SIGN, SCOPE.READ, SCOPE.STORE]
448
- if (scopes.includes(SCOPE.PASSWORDS)) {
449
- 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' })
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' })
450
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)
451
477
  if (typeof identity?.admitMember !== 'function') {
452
478
  throw Object.assign(new Error('this vault cannot add devices to the account record: no login was created'), { code: 'admit-unavailable' })
453
479
  }
454
480
  const deviceId = await deviceIdOf(pub)
455
- logins.registerFinish({ user, upload, pub, encPub, deviceId, label, blob })
481
+ await logins.registerFinish({ user, upload, pub, encPub, deviceId, label, blob })
456
482
  try {
457
483
  const { cert } = await identity.signDelegation(pub, scopes, { label: label || `login:${user}` })
458
- const cn = scopeToCn(scopes)
459
- const caps = [...new Set([...scopeToCaps(scopes), ...(unattended ? ['unattended'] : [])])]
460
- 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 })
461
485
  logins.setCert({ user, cert, iss: identity.me?.publickey || null })
462
- return { ok: true, user, deviceId, cert, caps }
486
+ return { ok: true, user, deviceId, cert, caps: pedidos }
463
487
  } catch (e) {
464
488
  // NADA DE MEDIAS ALTAS: si no entra en el acta, no queda un usuario que pueda entrar a
465
489
  // una cuenta que no lo reconoce. Se deshace y se dice.