feat(recovery): update delete recovery key and get recovery key endpoints (#2518), r=@rfk
This commit is contained in:
Родитель
b6908b9fb0
Коммит
4d109a05a7
72
docs/api.md
72
docs/api.md
|
@ -58,9 +58,11 @@ see [`mozilla/fxa-js-client`](https://github.com/mozilla/fxa-js-client).
|
|||
* [Recovery codes](#recovery-codes)
|
||||
* [GET /recoveryCodes (:lock: sessionToken)](#get-recoverycodes)
|
||||
* [POST /session/verify/recoveryCode (:lock: sessionToken)](#post-sessionverifyrecoverycode)
|
||||
* [Recovery keys](#recovery-keys)
|
||||
* [POST /recoveryKeys (:lock: sessionToken)](#post-recoverykeys)
|
||||
* [GET /recoveryKeys/{recoveryKeyId} (:lock: accountResetToken)](#get-recoverykeysrecoverykeyid)
|
||||
* [Recovery key](#recovery-key)
|
||||
* [POST /recoveryKey (:lock: sessionToken)](#post-recoverykey)
|
||||
* [GET /recoveryKey/:recoveryKeyId (:lock: accountResetToken)](#get-recoverykey)
|
||||
* [POST /recoveryKey/exists (:lock: sessionToken)](#post-recoverykeyexists)
|
||||
* [DELETE /recoveryKey (:lock: sessionToken)](#delete-recoverykey)
|
||||
* [Session](#session)
|
||||
* [POST /session/destroy (:lock: sessionToken)](#post-sessiondestroy)
|
||||
* [POST /session/reauth (:lock: sessionToken)](#post-sessionreauth)
|
||||
|
@ -285,6 +287,10 @@ for `code` and `errno` are:
|
|||
Recovery code not found.
|
||||
* `code: 400, errno: 157`:
|
||||
Unavailable device command.
|
||||
* `code: 400, errno: 158`:
|
||||
Recovery key not found.
|
||||
* `code: 400, errno: 159`:
|
||||
Recovery key is not valid.
|
||||
* `code: 503, errno: 201`:
|
||||
Service unavailable
|
||||
* `code: 503, errno: 202`:
|
||||
|
@ -2346,12 +2352,12 @@ Verify a session using a recovery code.
|
|||
<!--end-response-body-post-sessionverifyrecoverycode-remaining-->
|
||||
|
||||
|
||||
### Recovery keys
|
||||
### Recovery key
|
||||
|
||||
#### POST /recoveryKeys
|
||||
#### POST /recoveryKey
|
||||
|
||||
:lock: HAWK-authenticated with session token
|
||||
<!--begin-route-post-recoverykeys-->
|
||||
<!--begin-route-post-recoverykey-->
|
||||
Creates a new recovery key for a user.
|
||||
|
||||
Recovery keys are one-time-use tokens
|
||||
|
@ -2359,30 +2365,68 @@ that can be used to recover the user's kB
|
|||
if they forget their password.
|
||||
For more details, see the
|
||||
[recovery keys](recovery_keys.md) docs.
|
||||
<!--end-route-post-recoverykeys-->
|
||||
<!--end-route-post-recoverykey-->
|
||||
|
||||
##### Request body
|
||||
|
||||
* `recoveryKeyId`: *validators.recoveryKeyId*
|
||||
|
||||
<!--begin-request-body-post-recoverykeys-recoveryKeyId-->
|
||||
<!--begin-request-body-post-recoverykey-recoveryKeyId-->
|
||||
A unique identifier for this recovery key, derived from the key via HKDF.
|
||||
<!--end-request-body-post-recoverykeys-recoveryKeyId-->
|
||||
<!--end-request-body-post-recoverykey-recoveryKeyId-->
|
||||
|
||||
* `recoveryData`: *validators.recoveryData*
|
||||
|
||||
<!--begin-request-body-post-recoverykeys-recoveryData-->
|
||||
<!--begin-request-body-post-recoverykey-recoveryData-->
|
||||
An encrypted bundle containing the user's kB.
|
||||
<!--end-request-body-post-recoverykeys-recoveryData-->
|
||||
<!--end-request-body-post-recoverykey-recoveryData-->
|
||||
|
||||
|
||||
#### GET /recoveryKeys/{recoveryKeyId}
|
||||
#### GET /recoveryKey/:recoveryKeyId
|
||||
|
||||
:lock: HAWK-authenticated with account reset token
|
||||
<!--begin-route-get-recoverykeysrecoverykeyid-->
|
||||
<!--begin-route-get-recoverykeyrecoverykeyid-->
|
||||
Retrieve the account recovery data associated with the given recovery key.
|
||||
<!--end-route-get-recoverykeysrecoverykeyid-->
|
||||
<!--end-route-get-recoverykeyrecoverykeyid-->
|
||||
|
||||
##### Response body
|
||||
|
||||
* `recoveryData`: *string*
|
||||
|
||||
<!--begin-response-body-post-recoverykeyrecoverykeyid-recoverydata-->
|
||||
|
||||
<!--end-response-body-post-recoverykeyrecoverykeyid-recoverydata-->
|
||||
|
||||
#### POST /recoveryKey/exists
|
||||
:lock: HAWK-authenticated with session token
|
||||
<!--begin-route-post-recoverykeyexists-->
|
||||
This route checks to see if given user has setup an account recovery key.
|
||||
When used during the password reset flow, an email can be provided (instead
|
||||
of a sessionToken) to check for the status. However, when
|
||||
using an email, the request is rate limited.
|
||||
<!--end-route-post-recoverykeyexists-->
|
||||
|
||||
##### Request body
|
||||
|
||||
* `email`: *validators.email.required*
|
||||
|
||||
<!--begin-request-body-post-recoverykeyexists-email-->
|
||||
|
||||
<!--end-request-body-post-recoverykeyexists-email-->
|
||||
|
||||
##### Response body
|
||||
|
||||
* `status`: *boolean, required*
|
||||
|
||||
<!--begin-response-body-post-recoverykeyexists-email-->
|
||||
|
||||
<!--end-response-body-post-recoverykeyexists-email-->
|
||||
|
||||
#### DELETE /recoveryKey
|
||||
<!--begin-route-delete-recoverykey-->
|
||||
This route remove an account's recovery key. When the key is
|
||||
removed, it can no longer be used to restore an account's kB.
|
||||
<!--end-route-delete-recoverykey-->
|
||||
|
||||
### Session
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ Creating a new recovery key involves the following steps:
|
|||
* recover-data = JWE(recover-enc, {"alg": "dir", "enc": "A256GCM", "kid": recover-kid}, kB)
|
||||
* FxA web-content submits recovery data to FxA server for storage,
|
||||
associating it with the fingerprint (recover-kid)
|
||||
* `POST /recoveryKeys`, providing `recoveryKeyId` and `recoveryData` in the request body.
|
||||
* `POST /recoveryKey`, providing `recoveryKeyId` and `recoveryData` in the request body.
|
||||
|
||||
This scheme ensures someone in posession of the recovery key,
|
||||
can request the encrypted recovery data
|
||||
|
@ -42,7 +42,7 @@ as follows:
|
|||
* FxA web-content uses the recovery code to derive the fingerprint
|
||||
and encryption key (recover-kid and recover-enc as defined above).
|
||||
* FxA web-content requests recover-data from FxA server, providing recover-kid.
|
||||
* `GET /recoveryKeys/:recoveryKeyId`, authenticated with `accountResetToken`.
|
||||
* `GET /recoveryKey/:recoveryKeyId`, authenticated with `accountResetToken`.
|
||||
* Providing the `:recoveryKeyId` here proves that the user posesses the recovery key,
|
||||
while the `accountResetToken` proves that they control the email address
|
||||
of the account.
|
||||
|
|
22
lib/db.js
22
lib/db.js
|
@ -1288,7 +1288,7 @@ module.exports = (
|
|||
}
|
||||
|
||||
SAFE_URLS.createRecoveryKey = new SafeUrl(
|
||||
'/account/:uid/recoveryKeys',
|
||||
'/account/:uid/recoveryKey',
|
||||
'db.createRecoveryKey'
|
||||
)
|
||||
DB.prototype.createRecoveryKey = function (uid, recoveryKeyId, recoveryData) {
|
||||
|
@ -1298,23 +1298,29 @@ module.exports = (
|
|||
}
|
||||
|
||||
SAFE_URLS.getRecoveryKey = new SafeUrl(
|
||||
'/account/:uid/recoveryKeys/:recoveryKeyId',
|
||||
'/account/:uid/recoveryKey',
|
||||
'db.getRecoveryKey'
|
||||
)
|
||||
DB.prototype.getRecoveryKey = function (uid, recoveryKeyId) {
|
||||
DB.prototype.getRecoveryKey = function (uid) {
|
||||
log.trace({op: 'DB.getRecoveryKey', uid})
|
||||
|
||||
return this.pool.get(SAFE_URLS.getRecoveryKey, { uid, recoveryKeyId })
|
||||
return this.pool.get(SAFE_URLS.getRecoveryKey, {uid})
|
||||
.catch(err => {
|
||||
if (isNotFoundError(err)) {
|
||||
throw error.recoveryKeyNotFound()
|
||||
}
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
SAFE_URLS.deleteRecoveryKey = new SafeUrl(
|
||||
'/account/:uid/recoveryKeys/:recoveryKeyId',
|
||||
'db.createRecoveryKey'
|
||||
'/account/:uid/recoveryKey',
|
||||
'db.deleteRecoveryKey'
|
||||
)
|
||||
DB.prototype.deleteRecoveryKey = function (uid, recoveryKeyId) {
|
||||
DB.prototype.deleteRecoveryKey = function (uid) {
|
||||
log.trace({op: 'DB.deleteRecoveryKey', uid})
|
||||
|
||||
return this.pool.del(SAFE_URLS.deleteRecoveryKey, { uid, recoveryKeyId })
|
||||
return this.pool.del(SAFE_URLS.deleteRecoveryKey, { uid })
|
||||
}
|
||||
|
||||
|
||||
|
|
21
lib/error.js
21
lib/error.js
|
@ -70,6 +70,9 @@ var ERRNO = {
|
|||
RECOVERY_CODE_NOT_FOUND: 156,
|
||||
DEVICE_COMMAND_UNAVAILABLE: 157,
|
||||
|
||||
RECOVERY_KEY_NOT_FOUND: 158,
|
||||
RECOVERY_KEY_INVALID: 159,
|
||||
|
||||
SERVER_BUSY: 201,
|
||||
FEATURE_NOT_ENABLED: 202,
|
||||
BACKEND_SERVICE_FAILURE: 203,
|
||||
|
@ -794,6 +797,24 @@ AppError.unavailableDeviceCommand = () => {
|
|||
})
|
||||
}
|
||||
|
||||
AppError.recoveryKeyNotFound = () => {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: ERRNO.RECOVERY_KEY_NOT_FOUND,
|
||||
message: 'Recovery key not found.'
|
||||
})
|
||||
}
|
||||
|
||||
AppError.recoveryKeyInvalid = () => {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: ERRNO.RECOVERY_KEY_INVALID,
|
||||
message: 'Recovery key is not valid.'
|
||||
})
|
||||
}
|
||||
|
||||
AppError.backendServiceFailure = (service, operation) => {
|
||||
return new AppError({
|
||||
code: 500,
|
||||
|
|
|
@ -1058,7 +1058,7 @@ module.exports = (log, db, mailer, Password, config, customs, signinUtils, push)
|
|||
|
||||
function deleteRecoveryKey() {
|
||||
if (recoveryKeyId) {
|
||||
return db.deleteRecoveryKey(account.uid, recoveryKeyId)
|
||||
return db.deleteRecoveryKey(account.uid)
|
||||
}
|
||||
|
||||
return P.resolve()
|
||||
|
|
|
@ -55,7 +55,7 @@ module.exports = function (
|
|||
const unblockCodes = require('./unblock-codes')(log, db, mailer, config.signinUnblock, customs)
|
||||
const totp = require('./totp')(log, db, mailer, customs, config.totp)
|
||||
const recoveryCodes = require('./recovery-codes')(log, db, config.totp, customs, mailer)
|
||||
const recoveryKeys = require('./recovery-keys')(log, db, Password, config.verifierVersion, customs)
|
||||
const recoveryKey = require('./recovery-key')(log, db, Password, config.verifierVersion, customs)
|
||||
const util = require('./util')(
|
||||
log,
|
||||
config,
|
||||
|
@ -79,7 +79,7 @@ module.exports = function (
|
|||
totp,
|
||||
unblockCodes,
|
||||
util,
|
||||
recoveryKeys
|
||||
recoveryKey
|
||||
)
|
||||
v1Routes.forEach(r => { r.path = basePath + '/v1' + r.path })
|
||||
defaults.forEach(r => { r.path = basePath + r.path })
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
'use strict'
|
||||
|
||||
const errors = require('../error')
|
||||
const validators = require('./validators')
|
||||
const isA = require('joi')
|
||||
const butil = require('../crypto/butil')
|
||||
|
||||
module.exports = (log, db, Password, verifierVersion, customs) => {
|
||||
return [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/recoveryKey',
|
||||
options: {
|
||||
auth: {
|
||||
strategy: 'sessionToken'
|
||||
},
|
||||
validate: {
|
||||
payload: {
|
||||
recoveryKeyId: validators.recoveryKeyId,
|
||||
recoveryData: validators.recoveryData
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: async function (request) {
|
||||
log.begin('createRecoveryKey', request)
|
||||
|
||||
const uid = request.auth.credentials.uid
|
||||
const sessionToken = request.auth.credentials
|
||||
const {recoveryKeyId, recoveryData} = request.payload
|
||||
|
||||
return createRecoveryKey()
|
||||
.then(emitMetrics)
|
||||
.then(() => { return {} })
|
||||
|
||||
function createRecoveryKey() {
|
||||
if (sessionToken.tokenVerificationId) {
|
||||
throw errors.unverifiedSession()
|
||||
}
|
||||
|
||||
return db.createRecoveryKey(uid, recoveryKeyId, recoveryData)
|
||||
}
|
||||
|
||||
function emitMetrics() {
|
||||
log.info({
|
||||
op: 'account.recoveryKey.created',
|
||||
uid
|
||||
})
|
||||
|
||||
return request.emitMetricsEvent('recoveryKey.created', {uid})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/recoveryKey/{recoveryKeyId}',
|
||||
options: {
|
||||
auth: {
|
||||
strategy: 'accountResetToken'
|
||||
},
|
||||
validate: {
|
||||
params: {
|
||||
recoveryKeyId: validators.recoveryKeyId
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: async function (request) {
|
||||
log.begin('getRecoveryKey', request)
|
||||
|
||||
const uid = request.auth.credentials.uid
|
||||
const ip = request.app.clientAddress
|
||||
const recoveryKeyId = request.params.recoveryKeyId
|
||||
let recoveryData
|
||||
|
||||
return customs.checkAuthenticated('getRecoveryKey', ip, uid)
|
||||
.then(getRecoveryKey)
|
||||
.then(() => { return {recoveryData} })
|
||||
|
||||
function getRecoveryKey() {
|
||||
return db.getRecoveryKey(uid)
|
||||
.then((res) => {
|
||||
// `db.getRecoveryKey` doesn't require recoveryKeyId to retrieve
|
||||
// the recovery bundle, however, we should perform a security
|
||||
// check to ensure that the returned bundle contains the recoveryKeyId.
|
||||
if (! butil.buffersAreEqual(res.recoveryKeyId, recoveryKeyId)) {
|
||||
throw errors.recoveryKeyInvalid()
|
||||
}
|
||||
recoveryData = res.recoveryData
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/recoveryKey/exists',
|
||||
options: {
|
||||
auth: {
|
||||
mode: 'optional',
|
||||
strategy: 'sessionToken'
|
||||
},
|
||||
validate: {
|
||||
payload: {
|
||||
email: validators.email().optional()
|
||||
}
|
||||
},
|
||||
response: {
|
||||
schema: {
|
||||
exists: isA.boolean().required()
|
||||
}
|
||||
}
|
||||
},
|
||||
handler(request) {
|
||||
log.begin('recoveryKeyExists', request)
|
||||
|
||||
const email = request.payload.email
|
||||
let exists = false, uid
|
||||
|
||||
if (request.auth.credentials) {
|
||||
uid = request.auth.credentials.uid
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (! uid) {
|
||||
// If not using a sessionToken, an email is required to check
|
||||
// for a recovery key. This occurs when checking from the
|
||||
// password reset page and allows us to redirect the user to either
|
||||
// the regular password reset or account recovery password reset.
|
||||
if (! email) {
|
||||
throw errors.missingRequestParameter('email')
|
||||
}
|
||||
|
||||
return customs.check(request, email, 'recoveryKeyExists')
|
||||
.then(() => db.accountRecord(email))
|
||||
.then((result) => uid = result.uid)
|
||||
}
|
||||
|
||||
// When checking from `/settings` a sessionToken is required and the
|
||||
// request is not rate limited.
|
||||
})
|
||||
.then(() => {
|
||||
return db.getRecoveryKey(uid)
|
||||
.then((recoveryKey) => {
|
||||
if (recoveryKey) {
|
||||
exists = true
|
||||
}
|
||||
}, (err) => {
|
||||
if (err.errno === errors.ERRNO.RECOVERY_KEY_NOT_FOUND) {
|
||||
exists = false
|
||||
return
|
||||
}
|
||||
throw err
|
||||
})
|
||||
})
|
||||
.then(() => {
|
||||
return {exists}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'DELETE',
|
||||
path: '/recoveryKey',
|
||||
options: {
|
||||
auth: {
|
||||
strategy: 'sessionToken'
|
||||
}
|
||||
},
|
||||
handler(request) {
|
||||
log.begin('recoveryKeyDelete', request)
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const sessionToken = request.auth.credentials
|
||||
|
||||
if (sessionToken.tokenVerificationId) {
|
||||
throw errors.unverifiedSession()
|
||||
}
|
||||
|
||||
return db.deleteRecoveryKey(sessionToken.uid)
|
||||
.then(() => {
|
||||
return {}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
'use strict'
|
||||
|
||||
const errors = require('../error')
|
||||
const validators = require('./validators')
|
||||
|
||||
module.exports = (log, db, Password, verifierVersion, customs) => {
|
||||
return [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/recoveryKeys',
|
||||
options: {
|
||||
auth: {
|
||||
strategy: 'sessionToken'
|
||||
},
|
||||
validate: {
|
||||
payload: {
|
||||
recoveryKeyId: validators.recoveryKeyId,
|
||||
recoveryData: validators.recoveryData
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: async function (request) {
|
||||
log.begin('createRecoveryKey', request)
|
||||
|
||||
const uid = request.auth.credentials.uid
|
||||
const sessionToken = request.auth.credentials
|
||||
const {recoveryKeyId, recoveryData} = request.payload
|
||||
|
||||
return createRecoveryKey()
|
||||
.then(emitMetrics)
|
||||
.then(() => { return {} })
|
||||
|
||||
function createRecoveryKey() {
|
||||
if (sessionToken.tokenVerificationId) {
|
||||
throw errors.unverifiedSession()
|
||||
}
|
||||
|
||||
return db.createRecoveryKey(uid, recoveryKeyId, recoveryData)
|
||||
}
|
||||
|
||||
function emitMetrics() {
|
||||
log.info({
|
||||
op: 'account.recoveryKey.created',
|
||||
uid
|
||||
})
|
||||
|
||||
return request.emitMetricsEvent('recoveryKey.created', {uid})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/recoveryKeys/{recoveryKeyId}',
|
||||
options: {
|
||||
auth: {
|
||||
strategy: 'accountResetToken'
|
||||
},
|
||||
validate: {
|
||||
params: {
|
||||
recoveryKeyId: validators.recoveryKeyId
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: async function (request) {
|
||||
log.begin('getRecoveryKey', request)
|
||||
|
||||
const uid = request.auth.credentials.uid
|
||||
const ip = request.app.clientAddress
|
||||
const recoveryKeyId = request.params.recoveryKeyId
|
||||
let recoveryData
|
||||
|
||||
return customs.checkAuthenticated('getRecoveryKey', ip, uid)
|
||||
.then(getRecoveryKey)
|
||||
.then(() => { return {recoveryData} })
|
||||
|
||||
function getRecoveryKey() {
|
||||
return db.getRecoveryKey(uid, recoveryKeyId)
|
||||
.then((res) => recoveryData = res.recoveryData)
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -610,6 +610,7 @@ module.exports = function (log, config) {
|
|||
Mailer.prototype.recoveryEmail = function (message) {
|
||||
var templateName = 'recoveryEmail'
|
||||
var query = {
|
||||
uid: message.uid,
|
||||
token: message.token,
|
||||
code: message.code,
|
||||
email: message.email
|
||||
|
|
|
@ -2727,7 +2727,7 @@
|
|||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"fxa-auth-db-mysql": {
|
||||
"version": "git+https://github.com/mozilla/fxa-auth-db-mysql.git#b5c0f0e34b41c9464528f01ddd4bc8a28ed44571",
|
||||
"version": "git+https://github.com/mozilla/fxa-auth-db-mysql.git#29b9b4bc65784659ff103d77a390dc043e41992c",
|
||||
"from": "git+https://github.com/mozilla/fxa-auth-db-mysql.git#master",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -2772,9 +2772,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"JSONStream": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz",
|
||||
"integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.3.tgz",
|
||||
"integrity": "sha512-3Sp6WZZ/lXl+nTDoGpGWHEpTnnC6X5fnkolYZR6nwIfzbxxvA8utPWe1gCt7i0m9uVGsSz2IS8K8mJ7HmlduMg==",
|
||||
"requires": {
|
||||
"jsonparse": "^1.2.0",
|
||||
"through": ">=2.2.7 <3"
|
||||
|
@ -2786,9 +2786,9 @@
|
|||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
||||
},
|
||||
"acorn": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz",
|
||||
"integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ=="
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz",
|
||||
"integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ=="
|
||||
},
|
||||
"acorn-jsx": {
|
||||
"version": "3.0.1",
|
||||
|
@ -2811,9 +2811,9 @@
|
|||
"integrity": "sha1-anmQQ3ynNtXhKI25K9MmbV9csqo="
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz",
|
||||
"integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==",
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
|
||||
"integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es6-promisify": "^5.0.0"
|
||||
|
@ -2840,6 +2840,7 @@
|
|||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
|
||||
"integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"kind-of": "^3.0.2",
|
||||
"longest": "^1.0.1",
|
||||
|
@ -2978,9 +2979,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
|
||||
"integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
|
@ -3036,9 +3037,9 @@
|
|||
"integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8="
|
||||
},
|
||||
"buffer-from": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz",
|
||||
"integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA=="
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz",
|
||||
"integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ=="
|
||||
},
|
||||
"builtin-modules": {
|
||||
"version": "1.1.1",
|
||||
|
@ -3539,12 +3540,12 @@
|
|||
}
|
||||
},
|
||||
"dtrace-provider": {
|
||||
"version": "0.8.6",
|
||||
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.6.tgz",
|
||||
"integrity": "sha1-QooiOv4DQl0s1tY0f99AxmkDVj0=",
|
||||
"version": "0.8.7",
|
||||
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.7.tgz",
|
||||
"integrity": "sha1-3JObTT4GIM/gwc2APQ0tftBP/QQ=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"nan": "^2.3.3"
|
||||
"nan": "^2.10.0"
|
||||
}
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
|
@ -3567,17 +3568,17 @@
|
|||
}
|
||||
},
|
||||
"error-ex": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
|
||||
"integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
|
||||
"requires": {
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"es5-ext": {
|
||||
"version": "0.10.42",
|
||||
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.42.tgz",
|
||||
"integrity": "sha512-AJxO1rmPe1bDEfSR6TJ/FgMFYuTBhR5R57KW58iCkYACMyFbrkqVyzXSurYoScDGvgyMpk7uRF/lPUPPTmsRSA==",
|
||||
"version": "0.10.45",
|
||||
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.45.tgz",
|
||||
"integrity": "sha512-FkfM6Vxxfmztilbxxz5UKSD4ICMf5tSpRFtDNtkAhOxZ0EKtX6qwmXNyH/sFyIbX2P/nU5AMiA9jilWsUGJzCQ==",
|
||||
"requires": {
|
||||
"es6-iterator": "~2.0.3",
|
||||
"es6-symbol": "~3.1.1",
|
||||
|
@ -3741,7 +3742,7 @@
|
|||
},
|
||||
"eslint-plugin-fxa": {
|
||||
"version": "git+https://github.com/mozilla/eslint-plugin-fxa.git#e082927b4c6dc17d21414e35f4c94312adbaba92",
|
||||
"from": "eslint-plugin-fxa@git+https://github.com/mozilla/eslint-plugin-fxa.git#e082927b4c6dc17d21414e35f4c94312adbaba92"
|
||||
"from": "git+https://github.com/mozilla/eslint-plugin-fxa.git#master"
|
||||
},
|
||||
"espree": {
|
||||
"version": "3.5.4",
|
||||
|
@ -3886,12 +3887,14 @@
|
|||
}
|
||||
},
|
||||
"find-my-way": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-1.12.0.tgz",
|
||||
"integrity": "sha512-d7wZ0IeijAZDA/gvHjCNxxRTDCn5j9hnugcgEbNzYhofbDfogGhyRu93mtcJoAxeB1zemWTz9JB2JzNOar/qbA==",
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-1.15.1.tgz",
|
||||
"integrity": "sha512-cwR1IxkB1JIIGxWpX3TQC1U/51htT4dps536rno7fkszeSSevvZGkl1dpIANRNq+X6/VDSF/S4JAuDPSTepHBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-decode-uri-component": "^1.0.0"
|
||||
"fast-decode-uri-component": "^1.0.0",
|
||||
"safe-regex": "^1.1.0",
|
||||
"semver-store": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"find-up": {
|
||||
|
@ -4476,9 +4479,9 @@
|
|||
"integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk="
|
||||
},
|
||||
"hosted-git-info": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz",
|
||||
"integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw=="
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
|
||||
"integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w=="
|
||||
},
|
||||
"hpack.js": {
|
||||
"version": "2.1.6",
|
||||
|
@ -4528,9 +4531,9 @@
|
|||
}
|
||||
},
|
||||
"ignore": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.8.tgz",
|
||||
"integrity": "sha512-pUh+xUQQhQzevjRHHFqqcTy0/dP/kS9I8HSrUydhihjuD09W6ldVWFtIrwhXdUJHis3i2rZNqEHpZH/cbinFbg=="
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
|
||||
"integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug=="
|
||||
},
|
||||
"imurmurhash": {
|
||||
"version": "0.1.4",
|
||||
|
@ -4633,7 +4636,8 @@
|
|||
"is-buffer": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
|
||||
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
|
||||
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
|
||||
"optional": true
|
||||
},
|
||||
"is-builtin-module": {
|
||||
"version": "1.0.0",
|
||||
|
@ -4862,6 +4866,7 @@
|
|||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
||||
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-buffer": "^1.1.5"
|
||||
}
|
||||
|
@ -5126,7 +5131,8 @@
|
|||
"longest": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
|
||||
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
|
||||
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
|
||||
"optional": true
|
||||
},
|
||||
"loud-rejection": {
|
||||
"version": "1.6.0",
|
||||
|
@ -5435,12 +5441,12 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz",
|
||||
"integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==",
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
|
||||
"integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.14.0"
|
||||
"lodash": "^4.17.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7029,9 +7035,9 @@
|
|||
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz",
|
||||
"integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
|
||||
"integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
|
||||
"requires": {
|
||||
"p-try": "^1.0.0"
|
||||
}
|
||||
|
@ -7292,7 +7298,8 @@
|
|||
"repeat-string": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
|
||||
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
|
||||
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
|
||||
"optional": true
|
||||
},
|
||||
"repeating": {
|
||||
"version": "2.0.1",
|
||||
|
@ -7333,9 +7340,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
|
||||
"integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
|
@ -7405,9 +7412,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
|
||||
"integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
|
@ -7439,9 +7446,9 @@
|
|||
"integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg=="
|
||||
},
|
||||
"restify-errors": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/restify-errors/-/restify-errors-6.0.0.tgz",
|
||||
"integrity": "sha512-Ytrpbf0KQ2h7TSrcCqmtA8dybaLv/+H9GHfayBiewwpNuzTIcomvLOfRzR0e4u2VdUsqggbWBrNogwKum0uQIQ==",
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/restify-errors/-/restify-errors-6.1.1.tgz",
|
||||
"integrity": "sha512-QSwjp1b0pHB8QQQwqaPJu+VroGHAGX+HeHqz50awIb8334SAENCKeCI1VAhN099n4h0UVNupJ99ozx0pkHdqew==",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
|
@ -7450,9 +7457,9 @@
|
|||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
|
||||
"integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA=="
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -7477,6 +7484,12 @@
|
|||
"onetime": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"ret": {
|
||||
"version": "0.1.15",
|
||||
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
|
||||
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
|
||||
"dev": true
|
||||
},
|
||||
"right-align": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz",
|
||||
|
@ -7527,11 +7540,20 @@
|
|||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
|
||||
},
|
||||
"safe-json-stringify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.1.0.tgz",
|
||||
"integrity": "sha512-EzBtUaFH9bHYPc69wqjp0efJI/DPNHdFbGE3uIMn4sVbO0zx8vZ8cG4WKxQfOpUOKsQyGBiT2mTqnCw+6nLswA==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
|
||||
"integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
|
||||
"optional": true
|
||||
},
|
||||
"safe-regex": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
|
||||
"integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ret": "~0.1.10"
|
||||
}
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
|
@ -7571,6 +7593,12 @@
|
|||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
|
||||
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
|
||||
},
|
||||
"semver-store": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-store/-/semver-store-0.3.0.tgz",
|
||||
"integrity": "sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg==",
|
||||
"dev": true
|
||||
},
|
||||
"shelljs": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz",
|
||||
|
@ -7721,9 +7749,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz",
|
||||
"integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=",
|
||||
"version": "1.14.2",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",
|
||||
"integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asn1": "~0.2.3",
|
||||
|
@ -7733,6 +7761,7 @@
|
|||
"ecc-jsbn": "~0.1.1",
|
||||
"getpass": "^0.1.1",
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.0.2",
|
||||
"tweetnacl": "~0.14.0"
|
||||
}
|
||||
},
|
||||
|
@ -7773,9 +7802,9 @@
|
|||
}
|
||||
},
|
||||
"stringstream": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
|
||||
"integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=",
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz",
|
||||
"integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==",
|
||||
"dev": true
|
||||
},
|
||||
"strip-ansi": {
|
||||
|
@ -8012,18 +8041,11 @@
|
|||
"dev": true
|
||||
},
|
||||
"util": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
|
||||
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.11.0.tgz",
|
||||
"integrity": "sha512-5n12uMzKCjvB2HPFHnbQSjaqAa98L5iIXmHrZCLavuZVe0qe/SJGbDGWlpaHk5lnBkWRDO+dRu1/PgmUYKPPTw==",
|
||||
"requires": {
|
||||
"inherits": "2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"inherits": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
|
||||
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
|
||||
}
|
||||
"inherits": "2.0.3"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
|
|
|
@ -866,7 +866,7 @@ module.exports = config => {
|
|||
.then((token) => {
|
||||
return this.doRequest(
|
||||
'POST',
|
||||
this.baseURL + '/recoveryKeys',
|
||||
this.baseURL + '/recoveryKey',
|
||||
token,
|
||||
{
|
||||
recoveryKeyId,
|
||||
|
@ -881,7 +881,39 @@ module.exports = config => {
|
|||
.then((token) => {
|
||||
return this.doRequest(
|
||||
'GET',
|
||||
`${this.baseURL}/recoveryKeys/${recoveryKeyId}`,
|
||||
`${this.baseURL}/recoveryKey/${recoveryKeyId}`,
|
||||
token
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
ClientApi.prototype.getRecoveryKeyExistsWithSession = function (sessionTokenHex) {
|
||||
return tokens.SessionToken.fromHex(sessionTokenHex)
|
||||
.then((token) => {
|
||||
return this.doRequest(
|
||||
'POST',
|
||||
`${this.baseURL}/recoveryKey/exists`,
|
||||
token,
|
||||
{}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
ClientApi.prototype.getRecoveryKeyExistsWithEmail = function (email) {
|
||||
return this.doRequest(
|
||||
'POST',
|
||||
`${this.baseURL}/recoveryKey/exists`,
|
||||
undefined,
|
||||
{email}
|
||||
)
|
||||
}
|
||||
|
||||
ClientApi.prototype.deleteRecoveryKey = function (sessionTokenHex) {
|
||||
return tokens.SessionToken.fromHex(sessionTokenHex)
|
||||
.then((token) => {
|
||||
return this.doRequest(
|
||||
'DELETE',
|
||||
`${this.baseURL}/recoveryKey`,
|
||||
token
|
||||
)
|
||||
})
|
||||
|
|
|
@ -558,6 +558,18 @@ module.exports = config => {
|
|||
return this.api.getRecoveryKey(this.accountResetToken, recoveryKeyId)
|
||||
}
|
||||
|
||||
Client.prototype.getRecoveryKeyExists = function (email) {
|
||||
if (! email) {
|
||||
return this.api.getRecoveryKeyExistsWithSession(this.sessionToken)
|
||||
} else {
|
||||
return this.api.getRecoveryKeyExistsWithEmail(email)
|
||||
}
|
||||
}
|
||||
|
||||
Client.prototype.deleteRecoveryKey = function () {
|
||||
return this.api.deleteRecoveryKey(this.sessionToken)
|
||||
}
|
||||
|
||||
Client.prototype.resetAccountWithRecoveryKey = function (newPassword, kB, recoveryKeyId, headers, options = {}) {
|
||||
if (! this.accountResetToken) {
|
||||
throw new Error('call verifyPasswordResetCode before calling resetAccountWithRecoveryKey')
|
||||
|
|
|
@ -165,9 +165,8 @@ describe('/account/reset', function () {
|
|||
it('should have deleted recovery key', () => {
|
||||
assert.equal(mockDB.deleteRecoveryKey.callCount, 1)
|
||||
const args = mockDB.deleteRecoveryKey.args[0]
|
||||
assert.equal(args.length, 2, 'db.deleteRecoveryKey passed correct number of args')
|
||||
assert.equal(args.length, 1, 'db.deleteRecoveryKey passed correct number of args')
|
||||
assert.equal(args[0], uid, 'uid passed')
|
||||
assert.equal(args[1], mockRequest.payload.recoveryKeyId, 'recoveryKeyId passed')
|
||||
})
|
||||
|
||||
it('should have reset custom server', () => {
|
||||
|
|
|
@ -9,6 +9,7 @@ const getRoute = require('../../routes_helpers').getRoute
|
|||
const mocks = require('../../mocks')
|
||||
const P = require('../../../lib/promise')
|
||||
const sinon = require('sinon')
|
||||
const errors = require('../../../lib/error')
|
||||
|
||||
let log, db, customs, routes, route, request, response
|
||||
const email = 'test@email.com'
|
||||
|
@ -16,7 +17,7 @@ const recoveryKeyId = '000000'
|
|||
const recoveryData = '11111111111'
|
||||
const uid = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
||||
|
||||
describe('POST /recoveryKeys', () => {
|
||||
describe('POST /recoveryKey', () => {
|
||||
describe('should create recovery key', () => {
|
||||
beforeEach(() => {
|
||||
const requestOptions = {
|
||||
|
@ -24,7 +25,7 @@ describe('POST /recoveryKeys', () => {
|
|||
log,
|
||||
payload: {recoveryKeyId, recoveryData}
|
||||
}
|
||||
return setup({db: {}}, {}, '/recoveryKeys', requestOptions).then(r => response = r)
|
||||
return setup({db: {}}, {}, '/recoveryKey', requestOptions).then(r => response = r)
|
||||
})
|
||||
|
||||
it('returned the correct response', () => {
|
||||
|
@ -68,15 +69,15 @@ describe('POST /recoveryKeys', () => {
|
|||
const requestOptions = {
|
||||
credentials: {uid, email, tokenVerificationId: '1232311'},
|
||||
}
|
||||
return setup({db: {}}, {}, '/recoveryKeys', requestOptions)
|
||||
return setup({db: {}}, {}, '/recoveryKey', requestOptions)
|
||||
.then(assert.fail, (err) => {
|
||||
assert.deepEqual(err.errno, 138, 'returns unverified session error')
|
||||
assert.deepEqual(err.errno, errors.ERRNO.SESSION_UNVERIFIED, 'returns unverified session error')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /recoveryKeys/{recoveryKeyId}', () => {
|
||||
describe('GET /recoveryKey/{recoveryKeyId}', () => {
|
||||
describe('should get recovery key', () => {
|
||||
beforeEach(() => {
|
||||
const requestOptions = {
|
||||
|
@ -84,7 +85,7 @@ describe('GET /recoveryKeys/{recoveryKeyId}', () => {
|
|||
params: {recoveryKeyId},
|
||||
log
|
||||
}
|
||||
return setup({db: {recoveryData}}, {}, '/recoveryKeys/{recoveryKeyId}', requestOptions)
|
||||
return setup({db: {recoveryData, recoveryKeyId}}, {}, '/recoveryKey/{recoveryKeyId}', requestOptions)
|
||||
.then(r => response = r)
|
||||
})
|
||||
|
||||
|
@ -112,9 +113,143 @@ describe('GET /recoveryKeys/{recoveryKeyId}', () => {
|
|||
it('called db.getRecoveryKey correctly', () => {
|
||||
assert.equal(db.getRecoveryKey.callCount, 1)
|
||||
const args = db.getRecoveryKey.args[0]
|
||||
assert.equal(args.length, 2)
|
||||
assert.equal(args.length, 1)
|
||||
assert.equal(args[0], uid)
|
||||
assert.equal(args[1], recoveryKeyId)
|
||||
})
|
||||
})
|
||||
|
||||
describe('fails to return recovery data with recoveryKeyId mismatch', () => {
|
||||
beforeEach(() => {
|
||||
const requestOptions = {
|
||||
credentials: {uid, email},
|
||||
params: {recoveryKeyId},
|
||||
log
|
||||
}
|
||||
return setup({db: {recoveryData, recoveryKeyId: '11111'}}, {}, '/recoveryKey/{recoveryKeyId}', requestOptions)
|
||||
.then(assert.fail, (err) => response = err)
|
||||
})
|
||||
|
||||
it('returned the correct response', () => {
|
||||
assert.deepEqual(response.errno, errors.ERRNO.RECOVERY_KEY_INVALID, 'correct invalid recovery key errno')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /recoveryKey/exists', () => {
|
||||
describe('should check if recovery key exists using sessionToken', () => {
|
||||
beforeEach(() => {
|
||||
const requestOptions = {
|
||||
credentials: {uid, email},
|
||||
log
|
||||
}
|
||||
return setup({db: {recoveryData, }}, {}, '/recoveryKey/exists', requestOptions)
|
||||
.then(r => response = r)
|
||||
})
|
||||
|
||||
it('returned the correct response', () => {
|
||||
assert.deepEqual(response.exists, true, 'exists ')
|
||||
})
|
||||
|
||||
it('called log.begin correctly', () => {
|
||||
assert.equal(log.begin.callCount, 1)
|
||||
const args = log.begin.args[0]
|
||||
assert.equal(args.length, 2)
|
||||
assert.equal(args[0], 'recoveryKeyExists')
|
||||
assert.equal(args[1], request)
|
||||
})
|
||||
|
||||
it('called db.getRecoveryKey correctly', () => {
|
||||
assert.equal(db.getRecoveryKey.callCount, 1)
|
||||
const args = db.getRecoveryKey.args[0]
|
||||
assert.equal(args.length, 1)
|
||||
assert.equal(args[0], uid)
|
||||
})
|
||||
})
|
||||
|
||||
describe('should check if recovery key exists using email', () => {
|
||||
beforeEach(() => {
|
||||
const requestOptions = {
|
||||
payload: {email},
|
||||
log
|
||||
}
|
||||
return setup({db: {uid, email, recoveryData}}, {}, '/recoveryKey/exists', requestOptions)
|
||||
.then(r => response = r)
|
||||
})
|
||||
|
||||
it('returned the correct response', () => {
|
||||
assert.deepEqual(response.exists, true, 'exists ')
|
||||
})
|
||||
|
||||
it('called log.begin correctly', () => {
|
||||
assert.equal(log.begin.callCount, 1)
|
||||
const args = log.begin.args[0]
|
||||
assert.equal(args.length, 2)
|
||||
assert.equal(args[0], 'recoveryKeyExists')
|
||||
assert.equal(args[1], request)
|
||||
})
|
||||
|
||||
it('called customs.check correctly', () => {
|
||||
assert.equal(customs.check.callCount, 1)
|
||||
const args = customs.check.args[0]
|
||||
assert.equal(args.length, 3)
|
||||
assert.equal(args[1], email)
|
||||
assert.equal(args[2], 'recoveryKeyExists')
|
||||
})
|
||||
|
||||
it('called db.getRecoveryKey correctly', () => {
|
||||
assert.equal(db.getRecoveryKey.callCount, 1)
|
||||
const args = db.getRecoveryKey.args[0]
|
||||
assert.equal(args.length, 1)
|
||||
assert.equal(args[0], uid)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /recoveryKey', () => {
|
||||
describe('should delete recovery key', () => {
|
||||
beforeEach(() => {
|
||||
const requestOptions = {
|
||||
method: 'DELETE',
|
||||
credentials: {uid, email},
|
||||
log
|
||||
}
|
||||
return setup({db: {recoveryData}}, {}, '/recoveryKey', requestOptions)
|
||||
.then(r => response = r)
|
||||
})
|
||||
|
||||
it('returned the correct response', () => {
|
||||
assert.ok(response, 'empty response ')
|
||||
})
|
||||
|
||||
it('called log.begin correctly', () => {
|
||||
assert.equal(log.begin.callCount, 1)
|
||||
const args = log.begin.args[0]
|
||||
assert.equal(args.length, 2)
|
||||
assert.equal(args[0], 'recoveryKeyDelete')
|
||||
assert.equal(args[1], request)
|
||||
})
|
||||
|
||||
it('called db.deleteRecoveryKey correctly', () => {
|
||||
assert.equal(db.deleteRecoveryKey.callCount, 1)
|
||||
const args = db.deleteRecoveryKey.args[0]
|
||||
assert.equal(args.length, 1)
|
||||
assert.equal(args[0], uid)
|
||||
})
|
||||
})
|
||||
|
||||
describe('should fail for unverified session', () => {
|
||||
beforeEach(() => {
|
||||
const requestOptions = {
|
||||
method: 'DELETE',
|
||||
credentials: {uid, email, tokenVerificationId: 'unverified'},
|
||||
log
|
||||
}
|
||||
return setup({db: {recoveryData}}, {}, '/recoveryKey', requestOptions)
|
||||
.then(assert.fail, (err) => response = err)
|
||||
})
|
||||
|
||||
it('returned the correct response', () => {
|
||||
assert.equal(response.errno, errors.ERRNO.SESSION_UNVERIFIED, 'unverified session')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -127,7 +262,7 @@ function setup(results, errors, path, requestOptions) {
|
|||
db = mocks.mockDB(results.db, errors.db)
|
||||
customs = mocks.mockCustoms(errors.customs)
|
||||
routes = makeRoutes({log, db, customs})
|
||||
route = getRoute(routes, path)
|
||||
route = getRoute(routes, path, requestOptions.method)
|
||||
request = mocks.mockRequest(requestOptions)
|
||||
request.emitMetricsEvent = sinon.spy(() => P.resolve({}))
|
||||
return runTest(route, request)
|
||||
|
@ -139,7 +274,7 @@ function makeRoutes(options = {}) {
|
|||
const customs = options.customs || mocks.mockCustoms()
|
||||
const config = options.config || {signinConfirmation: {}}
|
||||
const Password = require('../../../lib/crypto/password')(log, config)
|
||||
return require('../../../lib/routes/recovery-keys')(log, db, Password, config.verifierVersion, customs)
|
||||
return require('../../../lib/routes/recovery-key')(log, db, Password, config.verifierVersion, customs)
|
||||
}
|
||||
|
||||
function runTest(route, request) {
|
||||
|
|
|
@ -345,6 +345,7 @@ function mockDB (data, errors) {
|
|||
}),
|
||||
getRecoveryKey: sinon.spy(() => {
|
||||
return P.resolve({
|
||||
recoveryKeyId: data.recoveryKeyId,
|
||||
recoveryData: data.recoveryData
|
||||
})
|
||||
}),
|
||||
|
@ -553,6 +554,7 @@ function mockRequest (data, errors) {
|
|||
},
|
||||
path: data.path,
|
||||
params: data.params || {},
|
||||
method: data.method || undefined,
|
||||
payload: data.payload || {},
|
||||
query: data.query || {},
|
||||
setMetricsFlowCompleteSignal: metricsContext.setFlowCompleteSignal,
|
||||
|
|
|
@ -75,6 +75,14 @@ describe('remote recovery keys', function () {
|
|||
})
|
||||
})
|
||||
|
||||
it('should fail to get unknown recovery key', () => {
|
||||
return getAccountResetToken(client, server, email)
|
||||
.then(() => client.getRecoveryKey('abce1234567890'))
|
||||
.then(assert.fail, (err) => {
|
||||
assert.equal(err.errno, 159, 'recovery key is not valid')
|
||||
})
|
||||
})
|
||||
|
||||
it('should fail if recoveryKeyId is missing', () => {
|
||||
return getAccountResetToken(client, server, email)
|
||||
.then(() => client.getRecoveryKey(recoveryKeyId))
|
||||
|
@ -122,6 +130,62 @@ describe('remote recovery keys', function () {
|
|||
})
|
||||
})
|
||||
|
||||
it('should delete recovery key', () => {
|
||||
return client.deleteRecoveryKey()
|
||||
.then((res) => {
|
||||
assert.ok(res, 'empty response')
|
||||
return client.getRecoveryKeyExists()
|
||||
.then((result) => {
|
||||
assert.equal(result.exists, false, 'recovery key deleted')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('check recovery key status', () => {
|
||||
describe('with sessionToken', () => {
|
||||
it('should return true if recovery key exists', () => {
|
||||
return client.getRecoveryKeyExists()
|
||||
.then((res) => {
|
||||
assert.equal(res.exists, true, 'recovery key exists')
|
||||
})
|
||||
})
|
||||
|
||||
it('should return false if recovery key doesn\'t exist', () => {
|
||||
email = server.uniqueEmail()
|
||||
return Client.createAndVerify(config.publicUrl, email, password, server.mailbox, {keys: true})
|
||||
.then((c) => {
|
||||
client = c
|
||||
return client.getRecoveryKeyExists()
|
||||
})
|
||||
.then((res) => {
|
||||
assert.equal(res.exists, false, 'recovery key doesnt exists')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with email', () => {
|
||||
it('should return true if recovery key exists', () => {
|
||||
return client.getRecoveryKeyExists(email)
|
||||
.then((res) => {
|
||||
assert.equal(res.exists, true, 'recovery key exists')
|
||||
})
|
||||
})
|
||||
|
||||
it('should return false if recovery key doesn\'t exist', () => {
|
||||
email = server.uniqueEmail()
|
||||
return Client.createAndVerify(config.publicUrl, email, password, server.mailbox, {keys: true})
|
||||
.then((c) => {
|
||||
client = c
|
||||
return client.getRecoveryKeyExists(email)
|
||||
})
|
||||
.then((res) => {
|
||||
assert.equal(res.exists, false, 'recovery key doesn\'t exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
after(() => {
|
||||
return TestServer.stop(server)
|
||||
})
|
||||
|
|
|
@ -4,12 +4,20 @@
|
|||
|
||||
'use strict'
|
||||
|
||||
exports.getRoute = function (routes, path) {
|
||||
exports.getRoute = function (routes, path, method) {
|
||||
var route = null
|
||||
|
||||
routes.some(function (r) {
|
||||
if (r.path === path) {
|
||||
route = r
|
||||
|
||||
if (method) {
|
||||
if (r.method === method) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
|
Загрузка…
Ссылка в новой задаче