@dotrino/identity 0.121.0 → 0.121.2
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/vault/remote.js +17 -2
- package/vault/vendor/passmanager/VERSION.txt +1 -1
- package/vault/vendor/passmanager/generate.js +1 -1
- package/vault/vendor/passmanager/import.js +3 -3
- package/vault/vendor/passmanager/totp.js +5 -5
- package/vault/vendor/passmanager/transport/proxy.js +3 -3
- package/vault/vendor/passmanager/vault/guard.js +1 -1
- package/vault/vendor/passmanager/vault/local.js +7 -7
- package/vault/vendor/passmanager/vault/remote.js +2 -2
- package/vault/vendor/passmanager/vault/responder.js +5 -5
- package/vault/vendor/passmanager/vault/sealed-local.js +2 -2
- package/vault/vendor/passmanager/vault/sealed.js +3 -3
- package/vault/vendor/proxy-client/VERSION.txt +1 -1
- package/vault/vendor/proxy-client/client.js +74 -47
- package/vault/vendor/proxy-client/encpub.js +2 -2
- package/vault/vendor/proxy-client/stats.js +7 -7
- package/vault/vendor/proxy-client/webrtc.js +10 -10
- package/vault/vendor/roadmap/VERSION.txt +1 -1
- package/vault/vendor/roadmap/index.js +4 -4
- package/vault/vendor/roadmap/manifests.js +46 -25
- package/vault/vendor/vault/VERSION.txt +1 -1
- package/vault/vendor/vault/core/libVersion.js +1 -1
- package/vault/vendor/vault/core/secretsStore.js +2 -2
- package/vault/vendor/vault/core/transport.js +21 -1
- package/vault/vendor/vault/core/vault.js +9 -2
- package/vault/vendor/vault/enroll.js +75 -27
- package/vault/vendor/vault/loginClient.js +10 -1
- package/vault/vendor/vault/passwordLogins.js +25 -2
|
@@ -143,6 +143,23 @@ export function createEnrollDesk ({
|
|
|
143
143
|
|
|
144
144
|
const fire = (fn, arg) => { try { fn(arg) } catch (_) {} }
|
|
145
145
|
const reply = (to, obj) => { try { send(to, obj) } catch (e) { log('[vault] could not reply:', e.message) } }
|
|
146
|
+
/**
|
|
147
|
+
* LO QUE LA BÓVEDA LE DICE AL APARATO MIENTRAS SE EMPAREJA VA FIRMADO CON LA MAESTRA, y
|
|
148
|
+
* con el `sn` de la sesión dentro. Lo que va a la cuenta lo reciben todas sus bóvedas y
|
|
149
|
+
* cualquiera que lo vea pasar puede contestar; sin firma, un `vault.error` de cualquiera
|
|
150
|
+
* tumbaba el alta (el iPhone, haciendo de bóveda, lo hacía sin querer, 2026-10-18). El
|
|
151
|
+
* aparato solo obedece lo que verifica contra la `iss` de su invitación (`fromMyVault`).
|
|
152
|
+
*/
|
|
153
|
+
// Lo firmado va en un bloque aparte (`signed`), como en el hello: quien envía estampa lo suyo
|
|
154
|
+
// en el sobre (versión, protocolo) y la firma tiene que cubrir exactamente lo que se verifica.
|
|
155
|
+
// Los campos de arriba (`error`, `deviceId`) son los de siempre, para quien los lee.
|
|
156
|
+
const tell = async (to, obj, sn) => {
|
|
157
|
+
const signed = { ...obj, sn: sn || null, iss, ts: Date.now() }
|
|
158
|
+
let signature
|
|
159
|
+
try { ({ signature } = await identity.signData(signed)) } catch (e) { log('[vault] could not sign the reply:', e.message); return }
|
|
160
|
+
reply(to, { ...obj, signed, vsig: signature })
|
|
161
|
+
}
|
|
162
|
+
const fail = (to, error, sn) => tell(to, { type: MSG_ERROR, error }, sn)
|
|
146
163
|
const isFresh = (d) => typeof d?.ts === 'number' && Math.abs(Date.now() - d.ts) <= FRESH_WINDOW_MS
|
|
147
164
|
|
|
148
165
|
/**
|
|
@@ -182,7 +199,9 @@ export function createEnrollDesk ({
|
|
|
182
199
|
if (!caps.length && !cn) throw Object.assign(new Error('the pairing grants no permission'), { code: 'empty-scope' })
|
|
183
200
|
caps = [...new Set(caps)]
|
|
184
201
|
}
|
|
185
|
-
|
|
202
|
+
// Uno a la vez: una sesión nueva supersede a la anterior. Las que se quitan se recuerdan
|
|
203
|
+
// como MÍAS: a un aparato que llegue con una de esas sí se le contesta que ya no vale.
|
|
204
|
+
for (const k of pending.keys()) forget(k)
|
|
186
205
|
const acct = String(account || '').slice(0, 40)
|
|
187
206
|
// INVITACIÓN CORTA: si sabemos cómo alcanzarnos, el QR lleva solo eso y el
|
|
188
207
|
// nonce de la sesión. La llave, el proxy y el nombre de la cuenta los pide el
|
|
@@ -206,7 +225,21 @@ export function createEnrollDesk ({
|
|
|
206
225
|
return { token, qr: { v: 2, iss, proxy, token, sn, m: mode, ...(acct ? { acct } : {}) }, expiresInMs: PAIRING_TTL_MS }
|
|
207
226
|
}
|
|
208
227
|
|
|
209
|
-
function stopPairing (token) {
|
|
228
|
+
function stopPairing (token) { forget(token) }
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Las sesiones que ESTA bóveda abrió y ya no valen (superadas, paradas o gastadas), por
|
|
232
|
+
* su token y su `sn`. Distingue «la abrí yo y ya no vale» (se contesta con el error) de
|
|
233
|
+
* «no sé nada de esa invitación» (se calla: la abrió otra bóveda de la cuenta).
|
|
234
|
+
*/
|
|
235
|
+
const gone = new Set()
|
|
236
|
+
function forget (key) {
|
|
237
|
+
const pend = pending.get(key)
|
|
238
|
+
pending.delete(key)
|
|
239
|
+
if (!pend) return
|
|
240
|
+
gone.add(pend.token); if (pend.sn) gone.add(pend.sn)
|
|
241
|
+
while (gone.size > 200) gone.delete(gone.values().next().value)
|
|
242
|
+
}
|
|
210
243
|
|
|
211
244
|
function listPending () {
|
|
212
245
|
return [...pending.values()]
|
|
@@ -229,9 +262,18 @@ export function createEnrollDesk ({
|
|
|
229
262
|
*/
|
|
230
263
|
async function handleHello (from, p) {
|
|
231
264
|
const pend = pending.get(String(p?.sn || ''))
|
|
265
|
+
// UNA INVITACIÓN QUE NO ES MÍA NO SE CONTESTA. Una cuenta tiene varias bóvedas a la vez
|
|
266
|
+
// (la del VPS, la pestaña, el teléfono que hace de bóveda), y lo que va a la cuenta les
|
|
267
|
+
// llega a todas. Si la que no abrió el emparejamiento contesta «no hay sesión», ese
|
|
268
|
+
// error le gana a la respuesta buena y el aparato se queda fuera (2026-10-18: el iPhone
|
|
269
|
+
// tumbaba cada alta de un servicio). Se anota y se calla: la que la abrió, contesta.
|
|
270
|
+
if (!pend && !gone.has(String(p?.sn || ''))) {
|
|
271
|
+
audit('ignored', { what: 'hello', reason: 'not-mine' })
|
|
272
|
+
return { ignored: true }
|
|
273
|
+
}
|
|
232
274
|
if (!pend || Date.now() > pend.exp) {
|
|
233
275
|
audit('rejected', { what: 'hello', reason: 'sin-sesion' })
|
|
234
|
-
return
|
|
276
|
+
return fail(from, 'no pairing session open for that code', String(p?.sn || ''))
|
|
235
277
|
}
|
|
236
278
|
// La respuesta va FIRMADA por la maestra y el `sn` va dentro de lo firmado. Eso ata
|
|
237
279
|
// la respuesta a ESTA sesión: no se puede reutilizar la de otro emparejamiento ni la
|
|
@@ -250,42 +292,48 @@ export function createEnrollDesk ({
|
|
|
250
292
|
async function handleEnroll (from, p) {
|
|
251
293
|
const d = p?.data
|
|
252
294
|
if (!d || typeof d.dpub !== 'string' || typeof p.signature !== 'string') {
|
|
253
|
-
return
|
|
295
|
+
return fail(from, 'invalid enroll', p?.data?.sn)
|
|
254
296
|
}
|
|
255
297
|
const pend = pending.get(d.token)
|
|
298
|
+
// Lo mismo que en el hello: una sesión que esta bóveda no abrió no es suya para
|
|
299
|
+
// rechazarla. Solo contesta por las que conoce (gastada o vencida).
|
|
300
|
+
if (!pend && !gone.has(String(d.token || ''))) {
|
|
301
|
+
audit('ignored', { what: 'enroll', reason: 'not-mine' })
|
|
302
|
+
return { ignored: true }
|
|
303
|
+
}
|
|
256
304
|
if (!pend || pend.state === 'DONE' || Date.now() > pend.exp) {
|
|
257
|
-
return
|
|
305
|
+
return fail(from, 'invalid or expired pairing token', d.sn)
|
|
258
306
|
}
|
|
259
|
-
if (d.sn !== pend.sn) return
|
|
307
|
+
if (d.sn !== pend.sn) return fail(from, 'invalid session', d.sn)
|
|
260
308
|
// V7 · la INTENCIÓN viaja firmada y tiene que coincidir con el modo con el que ESTA
|
|
261
309
|
// bóveda abrió el emparejamiento. Es lo que garantiza que lo que pasa es lo que el
|
|
262
310
|
// humano vio anunciado en las dos pantallas, y no algo que se decidió a mitad de camino.
|
|
263
311
|
const intent = d.intent || 'join'
|
|
264
312
|
if (intent !== 'join' && intent !== 'adopt') {
|
|
265
|
-
return
|
|
313
|
+
return fail(from, 'unknown intent: ' + intent, d.sn)
|
|
266
314
|
}
|
|
267
315
|
if (intent !== (pend.mode || 'join')) {
|
|
268
316
|
audit('rejected', { what: 'enroll', reason: 'intent-mismatch' })
|
|
269
|
-
return
|
|
317
|
+
return fail(from, `this pairing was opened for "${pend.mode || 'join'}" and the device asked for "${intent}"`, d.sn)
|
|
270
318
|
}
|
|
271
319
|
if (!isFresh(d)) {
|
|
272
320
|
audit('rejected', { what: 'enroll', reason: 'stale' })
|
|
273
|
-
return
|
|
321
|
+
return fail(from, 'stale request: ts outside the ±5 min window (possible replay, or the device clock is off)', d.sn)
|
|
274
322
|
}
|
|
275
323
|
// PRUEBA DE POSESIÓN: la firma de `data` debe verificar contra `dpub`.
|
|
276
324
|
if (!(await verifyDeviceSig({ publickey: d.dpub, data: d, signature: p.signature }))) {
|
|
277
325
|
audit('rejected', { what: 'enroll', reason: 'bad-device-signature' })
|
|
278
|
-
return
|
|
326
|
+
return fail(from, 'invalid device signature', d.sn)
|
|
279
327
|
}
|
|
280
328
|
// El COMPROMISO del código es obligatorio: sin él no se puede comprobar al aprobar
|
|
281
329
|
// y volveríamos a emitir certs a ciegas. Un cliente viejo cae acá con un mensaje claro.
|
|
282
330
|
if (typeof d.commit !== 'string' || !/^[0-9a-f]{64}$/.test(d.commit)) {
|
|
283
331
|
audit('rejected', { what: 'enroll', reason: 'no-commit' })
|
|
284
|
-
return
|
|
332
|
+
return fail(from, 'this device speaks an old pairing version (no code commitment). Update it and try again.', d.sn)
|
|
285
333
|
}
|
|
286
334
|
// Un solo dispositivo a la vez esperando su código (así aprobar no es ambiguo).
|
|
287
335
|
if (pend.state === 'PENDING_CONFIRM' && pend.dpub && pend.dpub !== d.dpub) {
|
|
288
|
-
return
|
|
336
|
+
return fail(from, 'another device is already using this pairing session', d.sn)
|
|
289
337
|
}
|
|
290
338
|
|
|
291
339
|
const deviceId = await deviceIdOf(d.dpub)
|
|
@@ -300,7 +348,7 @@ export function createEnrollDesk ({
|
|
|
300
348
|
// persona sí puede entrar sin ella: no lee variables de servicio.
|
|
301
349
|
if (typeof d.encPub === 'string') pend.encPub = d.encPub
|
|
302
350
|
if (!pend.encPub && scopeToCn(pend.scope)) {
|
|
303
|
-
return
|
|
351
|
+
return fail(from, 'a service must send its encryption key (update @dotrino/vault on the service)', d.sn)
|
|
304
352
|
}
|
|
305
353
|
// Certificado de continuidad (opcional): lo firma la identidad que se une, con su
|
|
306
354
|
// propia llave. Se comprueba aquí y se guarda con el miembro al aprobar.
|
|
@@ -322,7 +370,7 @@ export function createEnrollDesk ({
|
|
|
322
370
|
// llegue el acta sellada, que es la que este dispositivo dijo que iba a entregar.
|
|
323
371
|
if (intent === 'adopt' && typeof d.profileId === 'string') pend.profileId = d.profileId
|
|
324
372
|
|
|
325
|
-
|
|
373
|
+
await tell(from, { type: MSG_ENROLL_CHALLENGE, deviceId }, pend.sn)
|
|
326
374
|
fire(onChallenge, { deviceId, scope: pend.scope, label: pend.label || '', machine: pend.machine || null, mode: pend.mode || 'join' })
|
|
327
375
|
fire(onPendingChange)
|
|
328
376
|
return { deviceId }
|
|
@@ -410,8 +458,8 @@ export function createEnrollDesk ({
|
|
|
410
458
|
log('[vault] could not revoke the undelivered certificate %s: %s', cert.nonce, re.message)
|
|
411
459
|
}
|
|
412
460
|
audit('rejected', { what: 'approve', device: pend.deviceId, reason: 'admit-failed' })
|
|
413
|
-
|
|
414
|
-
|
|
461
|
+
await fail(pend.from, 'the vault could not add this device to the account: pairing failed, try again', pend.sn)
|
|
462
|
+
forget(pend.token)
|
|
415
463
|
fire(onPendingChange)
|
|
416
464
|
log('[vault] could not add %s to the record, no certificate was delivered: %s', pend.deviceId, e.message)
|
|
417
465
|
throw Object.assign(
|
|
@@ -427,7 +475,7 @@ export function createEnrollDesk ({
|
|
|
427
475
|
// El acta viaja con el cert: el dispositivo ya sabe de quién es el perfil al que entra.
|
|
428
476
|
reply(pend.from, { type: MSG_ENROLLED, code, cert, iss, acta: record })
|
|
429
477
|
pend.state = 'DONE'
|
|
430
|
-
|
|
478
|
+
forget(pend.token)
|
|
431
479
|
fire(onPendingChange)
|
|
432
480
|
log(`[vault] device approved: ${pend.deviceId}`)
|
|
433
481
|
return { ok: true, deviceId: pend.deviceId, cert }
|
|
@@ -451,22 +499,22 @@ export function createEnrollDesk ({
|
|
|
451
499
|
async function handleActaSealed (from, p) {
|
|
452
500
|
const record = p?.acta
|
|
453
501
|
const pend = [...pending.values()].find((x) => x.state === 'AWAITING_ACTA' && (x.from === from || x.dpub))
|
|
454
|
-
if (!pend) return
|
|
455
|
-
if (!record || typeof record !== 'object') return
|
|
502
|
+
if (!pend) return fail(from, 'no adoption awaiting a record', pend?.sn)
|
|
503
|
+
if (!record || typeof record !== 'object') return fail(from, 'record missing or unreadable', pend?.sn)
|
|
456
504
|
// Ya no hay campo `sealer`: se le pregunta al PERMISO. Es la misma comprobación —«¿me
|
|
457
505
|
// nombra a mí la que me mandan?»— dicha en el idioma nuevo, y con varios selladores la
|
|
458
506
|
// respuesta puede ser que sí para más de uno, que es lo correcto.
|
|
459
507
|
if (!canSeal(record, iss)) {
|
|
460
508
|
audit('rejected', { what: 'adopt', reason: 'not-sealer' })
|
|
461
|
-
return
|
|
509
|
+
return fail(from, 'that record does not let this vault seal it', pend?.sn)
|
|
462
510
|
}
|
|
463
511
|
if (record.sealedBy !== pend.dpub) {
|
|
464
512
|
audit('rejected', { what: 'adopt', reason: 'sealed-by-other' })
|
|
465
|
-
return
|
|
513
|
+
return fail(from, 'that record was not sealed by the device of this pairing', pend?.sn)
|
|
466
514
|
}
|
|
467
515
|
if (pend.profileId && record.profileId !== pend.profileId) {
|
|
468
516
|
audit('rejected', { what: 'adopt', reason: 'other-profile' })
|
|
469
|
-
return
|
|
517
|
+
return fail(from, 'that record belongs to an account other than the one the device announced', pend?.sn)
|
|
470
518
|
}
|
|
471
519
|
|
|
472
520
|
try {
|
|
@@ -478,26 +526,26 @@ export function createEnrollDesk ({
|
|
|
478
526
|
const mine = (await identity.profileActa?.())?.acta || record
|
|
479
527
|
reply(pend.from, { type: MSG_ACTA_ADOPTED, code: p.code, acta: mine })
|
|
480
528
|
pend.state = 'DONE'
|
|
481
|
-
|
|
529
|
+
forget(pend.token)
|
|
482
530
|
fire(onPendingChange)
|
|
483
531
|
fire(onAdopted, { deviceId: pend.deviceId, profileId: record.profileId, seq: mine.seq })
|
|
484
532
|
log('[vault] account adopted from device %s (profile %s)', pend.deviceId, record.profileId?.slice(0, 12))
|
|
485
533
|
return { ok: true, adopted: true, profileId: record.profileId, seq: mine.seq }
|
|
486
534
|
} catch (e) {
|
|
487
535
|
log('[vault] could not adopt the account: %s', e.message)
|
|
488
|
-
|
|
536
|
+
fail(pend.from, 'the vault could not adopt the account: ' + e.message, pend.sn)
|
|
489
537
|
return { ok: false, error: e.message }
|
|
490
538
|
}
|
|
491
539
|
}
|
|
492
540
|
|
|
493
541
|
/** Rechaza un enrolamiento pendiente. */
|
|
494
|
-
function reject (deviceId) {
|
|
542
|
+
async function reject (deviceId) {
|
|
495
543
|
const pend = deviceId
|
|
496
544
|
? findPending(deviceId)
|
|
497
545
|
: [...pending.values()].find((p) => p.state === 'PENDING_CONFIRM')
|
|
498
546
|
if (!pend) return { ok: false }
|
|
499
|
-
|
|
500
|
-
|
|
547
|
+
await fail(pend.from, 'pairing rejected', pend.sn)
|
|
548
|
+
forget(pend.token)
|
|
501
549
|
audit('reject', { device: pend.deviceId })
|
|
502
550
|
fire(onPendingChange)
|
|
503
551
|
log('[vault] dispositivo rechazado: %s', pend.deviceId)
|
|
@@ -44,6 +44,8 @@ const err = (code, message, extra = {}) => Object.assign(new Error(message), { c
|
|
|
44
44
|
|
|
45
45
|
/** Cuánto se espera a que una bóveda conteste antes de probar con la siguiente. */
|
|
46
46
|
export const REPLY_TIMEOUT_MS = 15_000
|
|
47
|
+
/** Lo que se espera a una bóveda que se está despertando: arrancar en frío y conectarse. */
|
|
48
|
+
export const WAKE_WAIT_MS = 30_000
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
51
|
* Escucha UNA respuesta de un token concreto. El `off` se suelta siempre —también al
|
|
@@ -98,7 +100,14 @@ export async function loginWithPassword ({ transport, address, password, label =
|
|
|
98
100
|
if (typeof password !== 'string' || !password) throw err('no-password', 'loginWithPassword: password required')
|
|
99
101
|
const { user, code, address: dir } = parseLoginAddress(address)
|
|
100
102
|
|
|
101
|
-
|
|
103
|
+
let tokens = await transport.list(vaultChannel(code))
|
|
104
|
+
// UNA BÓVEDA QUE DUERME (la de una Lambda): el proxio la está despertando y lo dice. Se
|
|
105
|
+
// espera a que se anuncie en vez de contestar que esa cuenta no existe.
|
|
106
|
+
const hasta = Date.now() + WAKE_WAIT_MS
|
|
107
|
+
while (!tokens.length && tokens.waking && Date.now() < hasta) {
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
109
|
+
tokens = await transport.list(vaultChannel(code))
|
|
110
|
+
}
|
|
102
111
|
if (!tokens.length) {
|
|
103
112
|
throw err('no-vault', 'no vault is answering for that address right now: turn yours on, or check the address')
|
|
104
113
|
}
|
|
@@ -24,8 +24,8 @@
|
|
|
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 opaquePorDefecto, suiteId as suitePorDefecto } from '@dotrino/opaque'
|
|
28
|
-
import { pubkeyId } from '@dotrino/identity/capabilities'
|
|
27
|
+
import { server as opaquePorDefecto, client as opaqueClientePorDefecto, suiteId as suitePorDefecto } from '@dotrino/opaque'
|
|
28
|
+
import { pubkeyId, makeDeviceKey, makeDeviceEncKey } from '@dotrino/identity/capabilities'
|
|
29
29
|
import { DEVICE_CAPS, capScope } from '@dotrino/identity/acta'
|
|
30
30
|
import { deviceIdOf } from './enroll.js'
|
|
31
31
|
import { bytesToB64url, b64urlToBytes } from './b64.js'
|
|
@@ -448,6 +448,29 @@ export function createLoginDesk ({ load, save, now = () => Date.now(), opaque =
|
|
|
448
448
|
* `caps` los PERMISOS del acta que lleva el aparato, cualquiera de `DEVICE_CAPS`;
|
|
449
449
|
* por defecto firmar, leer y guardar
|
|
450
450
|
*/
|
|
451
|
+
/**
|
|
452
|
+
* DAR DE ALTA UN USUARIO, DEL LADO DE QUIEN TIENE LA CONTRASEÑA: el registro OPAQUE se
|
|
453
|
+
* termina aquí y las llaves del aparato nacen aquí y salen ya cerradas. La bóveda solo ve
|
|
454
|
+
* `begin`/`finish`, que es su mitad; quien llama decide por dónde llegan (el canal local de
|
|
455
|
+
* la CLI, o la propia bóveda en el arranque de una Lambda).
|
|
456
|
+
*
|
|
457
|
+
* @param {object} opts
|
|
458
|
+
* `begin(req)` → `{ response }` (`register-begin` de la bóveda)
|
|
459
|
+
* `finish(req)` → lo que devuelva (`register-finish` de la bóveda)
|
|
460
|
+
*/
|
|
461
|
+
export async function createPasswordLogin ({ user, password, label, caps, begin, finish, opaque = opaqueClientePorDefecto } = {}) {
|
|
462
|
+
if (!isValidUser(user)) throw Object.assign(new Error('invalid user name'), { code: 'bad-user' })
|
|
463
|
+
if (typeof password !== 'string' || !password) throw Object.assign(new Error('a password is required'), { code: 'no-password' })
|
|
464
|
+
const start = opaque.registrationStart({ password })
|
|
465
|
+
const begun = await begin({ user, request: start.request })
|
|
466
|
+
// La bóveda contesta con su mitad; el registro se termina AQUÍ, con la contraseña.
|
|
467
|
+
const fin = opaque.registrationFinish({ state: start.state, response: begun.response, password })
|
|
468
|
+
const device = await makeDeviceKey({ label })
|
|
469
|
+
const enc = await makeDeviceEncKey()
|
|
470
|
+
const blob = await sealDeviceKeys(fin.exportKey, { sign: device.privateJwk, enc: enc.privateJwk })
|
|
471
|
+
return finish({ user, upload: fin.upload, pub: device.publickey, encPub: enc.publickey, label, blob, caps })
|
|
472
|
+
}
|
|
473
|
+
|
|
451
474
|
export async function registerLogin ({
|
|
452
475
|
identity, logins, user, upload, pub, encPub = null, label = '', blob,
|
|
453
476
|
caps = ['sign', 'read', 'store'], scope, replace = false
|