diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2719a4 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# Firefox Accounts OAuth Server + +This code is now managed in +the [fxa-auth-server](https://github.com/mozilla/fxa-auth-server) repository. +This repository is maintained for historical purposes only. + diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..2227369 --- /dev/null +++ b/docs/api.md @@ -0,0 +1 @@ +This documentation now lives [in the auth-server repo](https://github.com/mozilla/fxa-auth-server/blob/master/fxa-oauth-server/docs/api.md). diff --git a/docs/clients.md b/docs/clients.md new file mode 100644 index 0000000..44c0f7c --- /dev/null +++ b/docs/clients.md @@ -0,0 +1 @@ +This documentation now lives [in the auth-server repo](https://github.com/mozilla/fxa-auth-server/blob/master/fxa-oauth-server/docs/clients.md). diff --git a/docs/pkce.md b/docs/pkce.md new file mode 100644 index 0000000..d541245 --- /dev/null +++ b/docs/pkce.md @@ -0,0 +1 @@ +This documentation now lives [in the auth-server repo](https://github.com/mozilla/fxa-auth-server/blob/master/fxa-oauth-server/docs/pkce.md). diff --git a/docs/scopes.md b/docs/scopes.md new file mode 100644 index 0000000..dd324ab --- /dev/null +++ b/docs/scopes.md @@ -0,0 +1 @@ +This documentation now lives [in the auth-server repo](https://github.com/mozilla/fxa-auth-server/blob/master/fxa-oauth-server/docs/scopes.md). diff --git a/docs/service-clients.md b/docs/service-clients.md new file mode 100644 index 0000000..7621408 --- /dev/null +++ b/docs/service-clients.md @@ -0,0 +1 @@ +This documentation now lives [in the auth-server repo](https://github.com/mozilla/fxa-auth-server/blob/master/fxa-oauth-server/docs/service-clients.md). diff --git a/fxa-oauth-server/.circleci/config.yml b/fxa-oauth-server/.circleci/config.yml deleted file mode 100644 index 45bd065..0000000 --- a/fxa-oauth-server/.circleci/config.yml +++ /dev/null @@ -1,95 +0,0 @@ -# These environment variables must be set in CircleCI UI -# -# DOCKERHUB_REPO - docker hub repo, format: / -# DOCKER_EMAIL - login info for docker hub -# DOCKER_USER -# DOCKER_PASS -# -version: 2 -jobs: - build: - docker: - - image: circleci/node - - steps: - - checkout - - setup_remote_docker - - - run: - name: Create version.json - command: > - printf '{"version":{"hash":"%s","version":"%s","source":"https://github.com/%s/%s","build":"%s"}}\n' - "$CIRCLE_SHA1" - "$CIRCLE_TAG" - "$CIRCLE_PROJECT_USERNAME" - "$CIRCLE_PROJECT_REPONAME" - "$CIRCLE_BUILD_URL" - | tee config/version.json version.json - - store_artifacts: - path: version.json - - - run: - name: Build deployment container image - command: docker build -f Dockerfile-build -t fxa-oauth-server:build . - - - run: - name: Check npm install - command: docker run --rm -it fxa-oauth-server:build npm ls --production - - - run: - name: Build test container image - command: docker build -f Dockerfile-test -t fxa-oauth-server:test . - - - run: - name: Run MySQL - command: docker run -d --name=mydb -e MYSQL_ALLOW_EMPTY_PASSWORD=true -e MYSQL_ROOT_HOST=% -p 3306:3306 mysql/mysql-server:5.6 - background: true - - - run: - name: Run Memory DB Tests - command: docker run fxa-oauth-server:test npm test - - - run: - name: Run MySQL DB Tests - command: docker run --net=host -p 3306:3306 -e DB="mysql" fxa-oauth-server:test npm test - - - run: - name: Push to Dockerhub - command: | - if [ "${CIRCLE_BRANCH}" == "master" ]; then - DOCKER_TAG="latest" - fi - - if [[ "${CIRCLE_BRANCH}" == feature* ]] || [[ "${CIRCLE_BRANCH}" == dockerpush* ]]; then - DOCKER_TAG="${CIRCLE_BRANCH}" - fi - - if [ -n "${CIRCLE_TAG}" ]; then - DOCKER_TAG="$CIRCLE_TAG" - fi - - if [ -n "${DOCKER_TAG}" ]; then - echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin - echo ${DOCKERHUB_REPO}:${DOCKER_TAG} - docker tag fxa-oauth-server:build ${DOCKERHUB_REPO}:${DOCKER_TAG} - docker images - docker push ${DOCKERHUB_REPO}:${DOCKER_TAG} - fi - -workflows: - version: 2 - - # workflow jobs are _not_ run in tag builds by default - # we use filters to whitelist jobs that should be run for tags - - # workflow jobs are run in _all_ branch builds by default - # we use filters to blacklist jobs that shouldn't be run for a branch - - # see: https://circleci.com/docs/2.0/workflows/#git-tag-job-execution - - build-test-push: - jobs: - - build: - filters: - tags: - only: /.*/ diff --git a/fxa-oauth-server/.dockerignore b/fxa-oauth-server/.dockerignore deleted file mode 100644 index 6b8710a..0000000 --- a/fxa-oauth-server/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -.git diff --git a/fxa-oauth-server/.eslintrc b/fxa-oauth-server/.eslintrc deleted file mode 100644 index 038e5d5..0000000 --- a/fxa-oauth-server/.eslintrc +++ /dev/null @@ -1,10 +0,0 @@ -plugins: - - fxa -extends: plugin:fxa/server - -rules: - handle-callback-err: 0 - semi: [2, "always"] - -parserOptions: - ecmaVersion: 2017 diff --git a/fxa-oauth-server/.gitignore b/fxa-oauth-server/.gitignore deleted file mode 100644 index 48e3aed..0000000 --- a/fxa-oauth-server/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -npm-debug.log -node_modules -Thumbs.db -/coverage.html -config/key.json -config/oldKey.json -.nyc_output diff --git a/fxa-oauth-server/.travis.yml b/fxa-oauth-server/.travis.yml deleted file mode 100644 index 7cf1bcd..0000000 --- a/fxa-oauth-server/.travis.yml +++ /dev/null @@ -1,46 +0,0 @@ -language: node_js - -node_js: - - '8' - -dist: trusty -sudo: true - -addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-4.8 - - mysql-server-5.6 - - mysql-client-core-5.6 - - mysql-client-5.6 - -notifications: - email: - smcarthur@mozilla.com - jrgm@mozilla.com - irc: - channels: - - 'irc.mozilla.org#fxa-bots' - use_notice: false - skip_join: false - -env: - - CXX=g++-4.8 NODE_ENV=test DB=memory - - CXX=g++-4.8 NODE_ENV=test DB=mysql - -before_install: - - npm i -g npm@6 - - npm config set spin false - -before_script: - - "mysql -u root -NBe 'select version()'" - - "mysql -u root -e 'DROP DATABASE IF EXISTS fxa_oauth;'" - - "mysql -u root -e 'CREATE DATABASE fxa_oauth CHARACTER SET utf8 COLLATE utf8_unicode_ci;'" - -script: - - npm run outdated - - npm test - # HACK: ignore npm audit errors for now until we get them all fixed - - npm audit || true diff --git a/fxa-oauth-server/CHANGELOG.md b/fxa-oauth-server/CHANGELOG.md deleted file mode 100644 index da3597b..0000000 --- a/fxa-oauth-server/CHANGELOG.md +++ /dev/null @@ -1,1083 +0,0 @@ - -# [1.123.0](https://github.com/mozilla/fxa-oauth-server/compare/v1.122.0...v1.123.0) (2018-10-16) - - -### Bug Fixes - -* **db:** Drop foreign key constraints. ([7ee117c](https://github.com/mozilla/fxa-oauth-server/commit/7ee117c)) -* **db:** Fix case-consistency of SQL query from #612 ([9e55714](https://github.com/mozilla/fxa-oauth-server/commit/9e55714)), closes [#612](https://github.com/mozilla/fxa-oauth-server/issues/612) - - - - -# [1.122.0](https://github.com/mozilla/fxa-oauth-server/compare/v1.120.0...v1.122.0) (2018-10-02) - - -### Bug Fixes - -* **ci:** remove nsp (#602) ([64ade86](https://github.com/mozilla/fxa-oauth-server/commit/64ade86)), closes [#602](https://github.com/mozilla/fxa-oauth-server/issues/602) [#596](https://github.com/mozilla/fxa-oauth-server/issues/596) [#597](https://github.com/mozilla/fxa-oauth-server/issues/597) -* **key-data:** Correctly handle non-existent scopes when finding key data. ([34d9493](https://github.com/mozilla/fxa-oauth-server/commit/34d9493)) - -### Features - -* **openid:** add profileChangedAt to claims (#607), r=@rfk ([f6e93eb](https://github.com/mozilla/fxa-oauth-server/commit/f6e93eb)), closes [#607](https://github.com/mozilla/fxa-oauth-server/issues/607) - - - - -# [1.121.0](https://github.com/mozilla/fxa-oauth-server/compare/v1.120.0...v1.121.0) (2018-09-18) - - -### Bug Fixes - -* **ci:** remove nsp (#602) ([64ade86](https://github.com/mozilla/fxa-oauth-server/commit/64ade86)), closes [#602](https://github.com/mozilla/fxa-oauth-server/issues/602) [#596](https://github.com/mozilla/fxa-oauth-server/issues/596) [#597](https://github.com/mozilla/fxa-oauth-server/issues/597) -* **key-data:** Correctly handle non-existent scopes when finding key data. ([34d9493](https://github.com/mozilla/fxa-oauth-server/commit/34d9493)) - - - - -# [1.120.0](https://github.com/mozilla/fxa-oauth-server/compare/v1.117.0...v1.120.0) (2018-09-06) - - -### Bug Fixes - -* **authorization:** Correctly handle non-existing URL scopes during authorization. (#594) r=@vladiko ([21654a3](https://github.com/mozilla/fxa-oauth-server/commit/21654a3)), closes [#594](https://github.com/mozilla/fxa-oauth-server/issues/594) [#593](https://github.com/mozilla/fxa-oauth-server/issues/593) -* **ci:** Run MySQL tests in Circle (#586) r=@vbudhram ([4b1c4e4](https://github.com/mozilla/fxa-oauth-server/commit/4b1c4e4)), closes [#586](https://github.com/mozilla/fxa-oauth-server/issues/586) [#581](https://github.com/mozilla/fxa-oauth-server/issues/581) -* **scopes:** Document scope-handling rules, use shared code to enforce them. (#551); r=vbudhr ([237886d](https://github.com/mozilla/fxa-oauth-server/commit/237886d)), closes [#551](https://github.com/mozilla/fxa-oauth-server/issues/551) - -### Features - -* **openid:** add the openid connect `at_hash` value (#598), r=@rfk ([d08310e](https://github.com/mozilla/fxa-oauth-server/commit/d08310e)), closes [#598](https://github.com/mozilla/fxa-oauth-server/issues/598) - - - - -# [1.119.0](https://github.com/mozilla/fxa-oauth-server/compare/v1.117.0...v1.119.0) (2018-08-21) - - -### Bug Fixes - -* **authorization:** Correctly handle non-existing URL scopes during authorization. (#594) r=@vladiko ([21654a3](https://github.com/mozilla/fxa-oauth-server/commit/21654a3)), closes [#594](https://github.com/mozilla/fxa-oauth-server/issues/594) [#593](https://github.com/mozilla/fxa-oauth-server/issues/593) -* **ci:** Run MySQL tests in Circle (#586) r=@vbudhram ([4b1c4e4](https://github.com/mozilla/fxa-oauth-server/commit/4b1c4e4)), closes [#586](https://github.com/mozilla/fxa-oauth-server/issues/586) [#581](https://github.com/mozilla/fxa-oauth-server/issues/581) -* **scopes:** Document scope-handling rules, use shared code to enforce them. (#551); r=vbudhr ([237886d](https://github.com/mozilla/fxa-oauth-server/commit/237886d)), closes [#551](https://github.com/mozilla/fxa-oauth-server/issues/551) - - - - -# [1.117.0](https://github.com/mozilla/fxa-oauth-server/compare/v1.115.2...v1.117.0) (2018-07-24) - - -### Bug Fixes - -* **clients:** match the notes client with fxa-dev and other envs (#585); r=rfk ([e24a582](https://github.com/mozilla/fxa-oauth-server/commit/e24a582)), closes [#585](https://github.com/mozilla/fxa-oauth-server/issues/585) -* **config:** For dev, the openid issuer is http://127.0.0.1:3030 (#583) r=@vladikoff ([38e1d73](https://github.com/mozilla/fxa-oauth-server/commit/38e1d73)), closes [#583](https://github.com/mozilla/fxa-oauth-server/issues/583) [mozilla/fxa-content-server#6362](https://github.com/mozilla/fxa-content-server/issues/6362) -* **doc:** Putting a little emphasis on email first (#584) r=@shane-tomlinson ([8ad17c1](https://github.com/mozilla/fxa-oauth-server/commit/8ad17c1)), closes [#584](https://github.com/mozilla/fxa-oauth-server/issues/584) -* **purge:** add purgeExpiredTokensById to select, then delete by primary key (#580); r=rfk ([adfff65](https://github.com/mozilla/fxa-oauth-server/commit/adfff65)), closes [#580](https://github.com/mozilla/fxa-oauth-server/issues/580) - -### Features - -* **codes:** Delete authorization codes when revoking client access. (#578); r=philbooth ([b905b7c](https://github.com/mozilla/fxa-oauth-server/commit/b905b7c)), closes [#578](https://github.com/mozilla/fxa-oauth-server/issues/578) - - - - -# [1.116.0](https://github.com/mozilla/fxa-oauth-server/compare/v1.115.2...v1.116.0) (2018-07-11) - - -### Features - -* **codes:** Delete authorization codes when revoking client access. (#578); r=philbooth ([b905b7c](https://github.com/mozilla/fxa-oauth-server/commit/b905b7c)), closes [#578](https://github.com/mozilla/fxa-oauth-server/issues/578) - - - - -## [1.115.2](https://github.com/mozilla/fxa-oauth-server/compare/v1.115.1...v1.115.2) (2018-07-04) - - -### Bug Fixes - -* **mysql:** Correctly aggregate tokens by clientid. (#576) r=@vladikoff ([2c2cd22](https://github.com/mozilla/fxa-oauth-server/commit/2c2cd22)), closes [#576](https://github.com/mozilla/fxa-oauth-server/issues/576) - - - - -## [1.115.1](https://github.com/mozilla/fxa-oauth-server/compare/v1.115.0...v1.115.1) (2018-06-27) - - -### Bug Fixes - -* **tokens:** Avoid quadratic behaviour when listing active clients. (#9); r=vladikoff ([15c3065](https://github.com/mozilla/fxa-oauth-server/commit/15c3065)), closes [#9](https://github.com/mozilla/fxa-oauth-server/issues/9) - - - - -# [1.115.0](https://github.com/mozilla/fxa-oauth-server/compare/v1.113.1...v1.115.0) (2018-06-25) - - - -# [1.114.0](https://github.com/mozilla/fxa-oauth-server/compare/v1.113.1...v1.114.0) (2018-06-13) - - -### Bug Fixes - -* **docker:** base image node:8-alpine and upgrade to npm6 (#567) r=@jbuck,@vladikoff ([d4060be](https://github.com/mozilla/fxa-oauth-server/commit/d4060be)), closes [#567](https://github.com/mozilla/fxa-oauth-server/issues/567) - - - - -## [1.113.1](https://github.com/mozilla/fxa-oauth-server/compare/v1.113.0...v1.113.1) (2018-06-09) - - -### Bug Fixes - -* **pkce:** Don't require PKCE in the direct grant flow. (#566) r=@vladikoff ([d70fe6d](https://github.com/mozilla/fxa-oauth-server/commit/d70fe6d)), closes [#566](https://github.com/mozilla/fxa-oauth-server/issues/566) [#559](https://github.com/mozilla/fxa-oauth-server/issues/559) - -### Features - -* **authorization:** Require tokenVerified=true for key-bearing scopes. (#561) r=@vladikoff ([f9ad63e](https://github.com/mozilla/fxa-oauth-server/commit/f9ad63e)), closes [#561](https://github.com/mozilla/fxa-oauth-server/issues/561) [/github.com/mozilla-services/tokenserver/blob/master/tokenserver/views.py#L140](https://github.com//github.com/mozilla-services/tokenserver/blob/master/tokenserver/views.py/issues/L140) - - - - -# [1.113.0](https://github.com/mozilla/fxa-oauth-server/compare/v1.112.1...v1.113.0) (2018-05-30) - - - - - -## [1.112.1](https://github.com/mozilla/fxa-oauth-server/compare/v1.112.0...v1.112.1) (2018-05-17) - - -### Bug Fixes - -* **changelog:** update to latest changelog version (#556) ([bc9256e](https://github.com/mozilla/fxa-oauth-server/commit/bc9256e)), closes [#556](https://github.com/mozilla/fxa-oauth-server/issues/556) - -### Features - -* **ci:** move to CircleCI 2 (#554) r=@jbuck ([97e4f62](https://github.com/mozilla/fxa-oauth-server/commit/97e4f62)), closes [#554](https://github.com/mozilla/fxa-oauth-server/issues/554) - - - - -## 1.112.0 (2018-05-16) - - - -## 1.111.0 (2018-05-02) - - -#### Bug Fixes - -* **changelog:** automated changelog is borked (#542) r=@vladikoff ([d7437211](https://github.com/mozilla/fxa-oauth-server/commit/d7437211), closes [#524](https://github.com/mozilla/fxa-oauth-server/issues/524)) -* **oauth:** another notes dev client (#546) ([9d5ec8e5](https://github.com/mozilla/fxa-oauth-server/commit/9d5ec8e5)) -* **validation:** Allow redirect uris with existing query params. (#548); r=philbooth ([b93e6a16](https://github.com/mozilla/fxa-oauth-server/commit/b93e6a16)) - - -#### Features - -* **node:** update to node 8 (#544) r=@jrgm ([e9b08ae0](https://github.com/mozilla/fxa-oauth-server/commit/e9b08ae0)) -* **sync:** - * add oldsync scope (#550) r=@rfk ([f2e7bb47](https://github.com/mozilla/fxa-oauth-server/commit/f2e7bb47)) - * add local test client for sync (#549) ([61ed2e73](https://github.com/mozilla/fxa-oauth-server/commit/61ed2e73)) - - - -## 1.110.0 (2018-04-18) - - -#### Bug Fixes - -* **tests:** mock outstanding error logs in test suite r=@vladikoff ([6a5d3ceb](https://github.com/mozilla/fxa-oauth-server/commit/6a5d3ceb), closes [#334](https://github.com/mozilla/fxa-oauth-server/issues/334)) - - -#### Features - -* **authorization:** Directly return `code` in authorization response. (#541); r=philbooth ([7ad1e56f](https://github.com/mozilla/fxa-oauth-server/commit/7ad1e56f)) -* **email-first:** Add support for the email-first flow. (#540); r=philbooth,rfk ([cb11145e](https://github.com/mozilla/fxa-oauth-server/commit/cb11145e), closes [#539](https://github.com/mozilla/fxa-oauth-server/issues/539)) - - - -## 1.109.0 (2018-04-03) - -### Bug Fixes - -* **buffer:** #527 Migrate deprecated buffer calls (#528) r=@vladikoff ([fd85207](https://github.com/mozilla/fxa-oauth-server/commit/fd85207)), closes [#527](https://github.com/mozilla/fxa-oauth-server/issues/527) -* **node:** Use Node.js v6.14.0 (#537) ([f32a3d7](https://github.com/mozilla/fxa-oauth-server/commit/f32a3d7)) -* **route:** make email false by default (#533) r=@rfk ([aa68fb9](https://github.com/mozilla/fxa-oauth-server/commit/aa68fb9)) -* **scripts:** Fix varname typo in test runner script. (#535) ([02804a8](https://github.com/mozilla/fxa-oauth-server/commit/02804a8)), closes [(#535](https://github.com/(/issues/535) -* **tests:** mock outstanding error logs in test suite r=@vladikoff ([6a5d3ce](https://github.com/mozilla/fxa-oauth-server/commit/6a5d3ce)) - -### chore - -* **config:** add Notes trailing slash to redirect in dev.json (#536) ([e8bf2e5](https://github.com/mozilla/fxa-oauth-server/commit/e8bf2e5)) - -### Features - -* **amr:** Report `amr` and `acr` claims in the id_token. (#530); r=vbudhram ([8181f7f](https://github.com/mozilla/fxa-oauth-server/commit/8181f7f)) -* **email-first:** Add support for the email-first flow. (#540); r=philbooth,rfk ([cb11145](https://github.com/mozilla/fxa-oauth-server/commit/cb11145)), closes [#539](https://github.com/mozilla/fxa-oauth-server/issues/539) -* **oauth:** make server compatible with AppAuth (#534) r=@rfk ([ff9e422](https://github.com/mozilla/fxa-oauth-server/commit/ff9e422)) - - - - - -## 1.108.0 (2018-03-21) - - -#### Bug Fixes - -* **buffer:** #527 Migrate deprecated buffer calls (#528) r=@vladikoff ([fd852072](https://github.com/mozilla/fxa-oauth-server/commit/fd852072), closes [#527](https://github.com/mozilla/fxa-oauth-server/issues/527)) - - -#### Features - -* **amr:** Report `amr` and `acr` claims in the id_token. (#530); r=vbudhram ([8181f7f6](https://github.com/mozilla/fxa-oauth-server/commit/8181f7f6)) - - - -## 1.107.0 (2018-03-08) - - - -## 1.106.0 (2018-02-21) - - - -## 1.105.0 (2018-02-07) - - -#### Features - -* **openid:** Allow untrusted reliers to request `openid` scope. (#516), r=@vbudhram ([f764dc82](https://github.com/mozilla/fxa-oauth-server/commit/f764dc82)) - - - -## 1.104.0 (2018-01-24) - - -#### Bug Fixes - -* **config:** - * reverting 'mark config sentryDsn and mysql password sensitive (#511) r=@vladikof ([41bd7c00](https://github.com/mozilla/fxa-oauth-server/commit/41bd7c00)) - * mark config sentryDsn and mysql password sensitive (#511) r=@vladikoff ([d98fbcde](https://github.com/mozilla/fxa-oauth-server/commit/d98fbcde)) - - -#### Features - -* **auth:** Accept client credentials in the Authorization header. (#514); r=philbooth ([1c508078](https://github.com/mozilla/fxa-oauth-server/commit/1c508078)) -* **keys:** Check lastAuthAt freshness when fetching key data. (#506) r=@vladikoff ([e0de2f3b](https://github.com/mozilla/fxa-oauth-server/commit/e0de2f3b)) - - - -## 1.103.0 (2018-01-08) - - -#### Bug Fixes - -* **node:** use node 6.12.3 (#510) r=@vladikoff ([adc1fc02](https://github.com/mozilla/fxa-oauth-server/commit/adc1fc02)) - - - -### 1.100.2 (2017-12-04) - - -#### Bug Fixes - -* **tokens:** invalidate refresh tokens on client-token DELETE action (#508) ([df0ca82a](https://github.com/mozilla/fxa-oauth-server/commit/df0ca82a), closes [#507](https://github.com/mozilla/fxa-oauth-server/issues/507)) - - - -### 1.100.1 (2017-11-27) - - -#### Bug Fixes - -* **keys:** replace scope key TLD (#505) r=@rfk ([a5e6d8f4](https://github.com/mozilla/fxa-oauth-server/commit/a5e6d8f4)) - - -#### Features - -* **keys:** Check lastAuthAt freshness when fetching key data. (#502) r=@vladikoff ([855adee4](https://github.com/mozilla/fxa-oauth-server/commit/855adee4)) - - - -## 1.100.0 (2017-11-15) - - -#### Bug Fixes - -* **node:** use node 6.12.0 (#501) r=@vladikoff ([167c9734](https://github.com/mozilla/fxa-oauth-server/commit/167c9734)) - - -#### Features - -* **logs:** add sentry support (#499), r=@vbudhram ([ef34859b](https://github.com/mozilla/fxa-oauth-server/commit/ef34859b)) - - - -## 1.99.0 (2017-11-03) - - -#### Bug Fixes - -* **pkce:** match pkce implementation to specifications (#498) r=rfk ([cf1c836b](https://github.com/mozilla/fxa-oauth-server/commit/cf1c836b), closes [#495](https://github.com/mozilla/fxa-oauth-server/issues/495)) -* **travis:** run tests with 6 and 8 (#497) r=vladikoff ([a49b2727](https://github.com/mozilla/fxa-oauth-server/commit/a49b2727)) - - - -### 1.98.1 (2017-10-26) - - - -## 1.98.0 (2017-10-18) - - - -## 1.97.0 (2017-10-03) - - -#### Bug Fixes - -* **deps:** update newrelic and request r=@shane-tomlinson ([b6d6c93c](https://github.com/mozilla/fxa-oauth-server/commit/b6d6c93c)) - - -#### Features - -* **keys:** - * add key-data docs, move client_id into payload (#491); r=rfk ([a9152c35](https://github.com/mozilla/fxa-oauth-server/commit/a9152c35)) - * add keys_jwe support (#486) r=rfk ([6a4efd1b](https://github.com/mozilla/fxa-oauth-server/commit/6a4efd1b), closes [#484](https://github.com/mozilla/fxa-oauth-server/issues/484)) -* **scopes:** - * allow https:// scopes (#490); r=rfk ([f892bcba](https://github.com/mozilla/fxa-oauth-server/commit/f892bcba), closes [#489](https://github.com/mozilla/fxa-oauth-server/issues/489)) - * add key-data and scope support (#487) r=rfk ([f3fcae5a](https://github.com/mozilla/fxa-oauth-server/commit/f3fcae5a), closes [#483](https://github.com/mozilla/fxa-oauth-server/issues/483)) - - - -## 1.96.0 (2017-09-19) - - -#### Features - -* **tokens:** add support for password change and reset event (#485) r=rfk ([f5873f9d](https://github.com/mozilla/fxa-oauth-server/commit/f5873f9d), closes [#481](https://github.com/mozilla/fxa-oauth-server/issues/481)) - - - -### 1.95.1 (2017-09-14) - - - -## 1.95.0 (2017-09-06) - - - -## 1.94.0 (2017-08-23) - - -#### Bug Fixes - -* **newrelic:** update to v2.1.0 ([87a3aeee](https://github.com/mozilla/fxa-oauth-server/commit/87a3aeee)) - - -#### Features - -* **pkce:** add ability for PKCE clients to use refresh_tokens (#476) r=seanmonstar ([7b401ebf](https://github.com/mozilla/fxa-oauth-server/commit/7b401ebf), closes [#472](https://github.com/mozilla/fxa-oauth-server/issues/472)) - - - -## 1.92.0 (2017-07-26) - - - -## 1.91.0 (2017-07-12) - - -#### Bug Fixes - -* **nodejs:** update to 6.11.1 for security fixes ([a0520c0c](https://github.com/mozilla/fxa-oauth-server/commit/a0520c0c)) - - -#### Features - -* **node:** upgrade to node 6 ([57c61ab1](https://github.com/mozilla/fxa-oauth-server/commit/57c61ab1)) - - - -## 1.90.0 (2017-06-28) - - -#### Features - -* **pkce:** add PKCE support to the oauth server (#466) r=seanmonstar ([ed59c0e6](https://github.com/mozilla/fxa-oauth-server/commit/ed59c0e6)) - - - -## 1.89.0 (2017-06-14) - - -#### Bug Fixes - -* **tests:** - * double before hook timeout for tests on slow machines ([23334169](https://github.com/mozilla/fxa-oauth-server/commit/23334169)) - * speed up and upgrade the test runner (#467) r=seanmonstar ([2e76c9e4](https://github.com/mozilla/fxa-oauth-server/commit/2e76c9e4)) - - -#### Features - -* **docker:** support feature branches (#464) r=jrgm ([f94fd61a](https://github.com/mozilla/fxa-oauth-server/commit/f94fd61a)) - - - -## 1.86.0 (2017-05-03) - - - -## 1.85.0 (2017-04-19) - - -#### Bug Fixes - -* **config:** - * expose clients config as OAUTH_CLIENTS ([04ebf6fd](https://github.com/mozilla/fxa-oauth-server/commit/04ebf6fd)) - * Add environment config options ([14a9b4a6](https://github.com/mozilla/fxa-oauth-server/commit/14a9b4a6)) -* **patcher:** Fix patcher with no pre-loaded clients ([dcc47b98](https://github.com/mozilla/fxa-oauth-server/commit/dcc47b98)) - - -#### Features - -* **lb:** Add `__lbheartbeat__` endpoint (#458), r=@jbuck ([c387907c](https://github.com/mozilla/fxa-oauth-server/commit/c387907c)) - - - -### 1.84.1 (2017-04-05) - - - -## 1.84.0 (2017-04-04) - - -#### Bug Fixes - -* **config:** expose more environment variables for config ([7a1dd19e](https://github.com/mozilla/fxa-oauth-server/commit/7a1dd19e)) -* **test:** fix unhandled rejection error with memory db impl (#454) r=vladikoff ([c870eba4](https://github.com/mozilla/fxa-oauth-server/commit/c870eba4)) - - -#### Features - -* **scripts:** Add script to generate an oauth client ([f21f657a](https://github.com/mozilla/fxa-oauth-server/commit/f21f657a)) - - - -## 1.83.0 (2017-03-21) - - -#### Bug Fixes - -* **tests:** check insert of utf8mb4 ([4e6a77a8](https://github.com/mozilla/fxa-oauth-server/commit/4e6a77a8)) -* **version:** use cwd and env var to get version (#452) r=vladikoff ([a3b1aa28](https://github.com/mozilla/fxa-oauth-server/commit/a3b1aa28)) - - -#### Features - -* **keys:** Add created-at timestamp to our public keys. (#453); r=seanmonstar,vladikoff ([511d9a63](https://github.com/mozilla/fxa-oauth-server/commit/511d9a63)) - - - -## 1.81.0 (2017-02-24) - - -#### Bug Fixes - -* **api:** clean up response of client-tokens delete endpoint (#3) (#449); r=rfk ([9c632731](https://github.com/mozilla/fxa-oauth-server/commit/9c632731)) -* **db:** ensure strict mode (#448) r=rfk,seanmonstar ([8d309c5b](https://github.com/mozilla/fxa-oauth-server/commit/8d309c5b), closes [#446](https://github.com/mozilla/fxa-oauth-server/issues/446)) -* **logs:** add scope and client_id logs to verify route (#447) r=seanmonstar ([33eb39ec](https://github.com/mozilla/fxa-oauth-server/commit/33eb39ec), closes [#444](https://github.com/mozilla/fxa-oauth-server/issues/444)) - - - -## 0.80.0 (2017-02-07) - -#### Features - -* **client:** scope is now returned in client-tokens (#445) r=vladikoff ([4efc383effc80](https://github.com/mozilla/fxa-oauth-server/commit/4efc383effc80)) - - -## 0.79.0 (2017-01-25) - - -#### Bug Fixes - -* **headers:** - * make "cache-control" value configurable ([5ba82ea6](https://github.com/mozilla/fxa-oauth-server/commit/5ba82ea6)) - * add cache-control headers to api endpoints; extend tests ([5a81ef94](https://github.com/mozilla/fxa-oauth-server/commit/5a81ef94)) -* **keys:** Generate unique 'kid' field when regenerating JWK keys ([5b9acae3](https://github.com/mozilla/fxa-oauth-server/commit/5b9acae3)) -* **scripts:** Use pure JS module to generate RSA keypairs (#439) r=vladikoff ([3380e1cc](https://github.com/mozilla/fxa-oauth-server/commit/3380e1cc)) - - -#### Features - -* **docker:** Shrink Docker image size (#438) r=vladikoff ([13d13b9e](https://github.com/mozilla/fxa-oauth-server/commit/13d13b9e)) - - - -## 0.78.0 (2017-01-11) - - -#### Bug Fixes - -* **security:** - * set x-frame-options deny ([21ea05dd](https://github.com/mozilla/fxa-oauth-server/commit/21ea05dd)) - * enable X-XSS-Protection 1; mode=block ([52ca1e56](https://github.com/mozilla/fxa-oauth-server/commit/52ca1e56)) - * enable x-content-type-options nosniff ([5ea5001c](https://github.com/mozilla/fxa-oauth-server/commit/5ea5001c)) - - - -## 0.77.0 (2017-01-04) - - -#### Bug Fixes - -* **codes:** Remove authorization codes after use. ([e0f8961d](https://github.com/mozilla/fxa-oauth-server/commit/e0f8961d)) -* **memorydb:** token createdAt used instead of client createdAt (#436) r=vladikoff,seanmonstar ([02dec664](https://github.com/mozilla/fxa-oauth-server/commit/02dec664), closes [#421](https://github.com/mozilla/fxa-oauth-server/issues/421)) -* **tokens:** Begin expiring access tokens beyond a configurable epoch. ([b3463264](https://github.com/mozilla/fxa-oauth-server/commit/b3463264)) - - - -## 0.76.0 (2016-12-13) - - -#### Bug Fixes - -* **deps:** update to hapi 16, add srinkwrap scripts, update other prod deps ([c102046e](https://github.com/mozilla/fxa-oauth-server/commit/c102046e)) - - -#### Features - -* **authorization:** add uri validation on the authorization endpoint (#428) r=jrgm,seanmonstar ([fcc0b52a](https://github.com/mozilla/fxa-oauth-server/commit/fcc0b52a), closes [#387](https://github.com/mozilla/fxa-oauth-server/issues/387), [#388](https://github.com/mozilla/fxa-oauth-server/issues/388)) - - - -## 0.75.0 (2016-11-30) - - -#### Bug Fixes - -* **tokens:** ttl parameter must be positive (#429) r=vladikoff ([1764d73a](https://github.com/mozilla/fxa-oauth-server/commit/1764d73a)) - - -#### Features - -* **hpkp:** Add the hpkp headers to all requests (#416) r=vladikoff ([6b8a8c86](https://github.com/mozilla/fxa-oauth-server/commit/6b8a8c86)) - - - -## 0.73.0 (2016-11-02) - - -#### Bug Fixes - -* **deps:** update to hapi 14 and joi 9 ([9bc87c01](https://github.com/mozilla/fxa-oauth-server/commit/9bc87c01), closes [#424](https://github.com/mozilla/fxa-oauth-server/issues/424)) -* **travis:** test on node4/node6 with default npm & g++-4.8 ([b4e1dd8e](https://github.com/mozilla/fxa-oauth-server/commit/b4e1dd8e)) - - - -## 0.71.0 (2016-10-05) - - -#### Features - -* **docker:** Add CloudOps Dockerfile & CircleCI build instructions ([a80b4b47](https://github.com/mozilla/fxa-oauth-server/commit/a80b4b47)) -* **shared:** add new locales ([d6e88df0](https://github.com/mozilla/fxa-oauth-server/commit/d6e88df0)) - - - -## 0.70.0 (2016-09-21) - - -#### Bug Fixes - -* **purge-expired:** - * accept a list of pocket-id's ([1c843a93](https://github.com/mozilla/fxa-oauth-server/commit/1c843a93)) - * Promise.delay takes milliseconds; allow subsecond delay ([10c61034](https://github.com/mozilla/fxa-oauth-server/commit/10c61034)) - * moar logging ([80c360e7](https://github.com/mozilla/fxa-oauth-server/commit/80c360e7)) - * set db.autoUpdateClients config to false ([bc66fc37](https://github.com/mozilla/fxa-oauth-server/commit/bc66fc37)) - * use db.getClient() to check for unknown clientId ([c33f1d9c](https://github.com/mozilla/fxa-oauth-server/commit/c33f1d9c)) - * log uncaughtException; minimum log level of info ([264271ef](https://github.com/mozilla/fxa-oauth-server/commit/264271ef)) - - - -## 0.69.0 (2016-09-08) - - -#### Bug Fixes - -* **log:** add remoteAddressChain to summary (#417) ([568cfa64](https://github.com/mozilla/fxa-oauth-server/commit/568cfa64), closes [#415](https://github.com/mozilla/fxa-oauth-server/issues/415)) - - -#### Features - -* **oauth:** - * add methods to support oauth client management (#405) r=seanmonstar ([27485107](https://github.com/mozilla/fxa-oauth-server/commit/27485107)) - * Track last time refreshToken was used (#412) r=vladikoff,seanmonstar ([25c455a6](https://github.com/mozilla/fxa-oauth-server/commit/25c455a6), closes [#275](https://github.com/mozilla/fxa-oauth-server/issues/275)) - - - -## 0.68.0 (2016-08-24) - - -#### Bug Fixes - -* **log:** avoid crashing on bad payload (#411) r=rfk,jrgm ([19ebed51](https://github.com/mozilla/fxa-oauth-server/commit/19ebed51), closes [#410](https://github.com/mozilla/fxa-oauth-server/issues/410)) -* **test:** encrypt refresh_token on db query (#414) r=seanmonstar,vladikoff ([7f52d46d](https://github.com/mozilla/fxa-oauth-server/commit/7f52d46d)) - - - -## 0.66.0 (2016-07-27) - - -#### Bug Fixes - -* **deps:** update some dependencies ([09aa7b0e](https://github.com/mozilla/fxa-oauth-server/commit/09aa7b0e)) -* **spelling:** minor spelling fix in tests (#403) r=vladikoff ([d4ff105b](https://github.com/mozilla/fxa-oauth-server/commit/d4ff105b)) - - - -## 0.65.0 (2016-07-13) - - -#### Bug Fixes - -* **scopes:** Dont treat `foo:write` as a sub-scope of `foo`. ([b4b30c29](https://github.com/mozilla/fxa-oauth-server/commit/b4b30c29)) -* **tokens:** Added scripts that purge expired access tokens. ([10bbb240](https://github.com/mozilla/fxa-oauth-server/commit/10bbb240)) - - - - -## 0.64.0 (2016-07-02) - - -#### Bug Fixes - -* **scopes:** Dont treat `foo:write` as a sub-scope of `foo`. ([fe2f1fef](https://github.com/mozilla/fxa-oauth-server/commit/fe2f1fef)) - - - -## 0.61.0 (2016-05-04) - -* **travis:** drop node 0.12 support ([b4eba468](https://github.com/mozilla/fxa-oauth-server/commit/b4eba468)) - - -## 0.59.0 (2016-03-30) - - - -## 0.57.0 (2016-03-05) - - -#### Bug Fixes - -* **db:** Fix an old db patch to apply cleanly in local dev. ([c7fa6336](https://github.com/mozilla/fxa-oauth-server/commit/c7fa6336)) -* **dependencies:** switch back to main generate-rsa-keypair now that my fix to it was merged ([1c1268b0](https://github.com/mozilla/fxa-oauth-server/commit/1c1268b0)) -* **shrinkwrap:** restore deleted npm-shrinkwrap.json ([63834811](https://github.com/mozilla/fxa-oauth-server/commit/63834811)) -* **tests:** - * More reliable generation of RSA keys for tests ([981d0b7c](https://github.com/mozilla/fxa-oauth-server/commit/981d0b7c)) - * Refactor use of process.exit() to be outside of code under test. ([47f4f176](https://github.com/mozilla/fxa-oauth-server/commit/47f4f176)) -* **validation:** Restrict characters allowed in 'scope' parameter. ([7dd2a391](https://github.com/mozilla/fxa-oauth-server/commit/7dd2a391)) - - - -## 0.56.0 (2016-02-10) - - -#### Bug Fixes - -* **openid:** Generate openid keys on npm postinstall to file ([5f15afaa](https://github.com/mozilla/fxa-oauth-server/commit/5f15afaa)) - - -#### Features - -* **clients:** Added initial support for using previous client secret ([4f9df20c](https://github.com/mozilla/fxa-oauth-server/commit/4f9df20c)) -* **docker:** Additional Dockerfile for self-hosting ([83a8b6c1](https://github.com/mozilla/fxa-oauth-server/commit/83a8b6c1)) - - - -### 0.53.1 (2016-01-11) - - - -## 0.53.0 (2016-01-04) - - -#### Bug Fixes - -* **deps:** switch from URIjs to urijs ([ecdf31ed](https://github.com/mozilla/fxa-oauth-server/commit/ecdf31ed), closes [#347](https://github.com/mozilla/fxa-oauth-server/issues/347)) -* **travis:** build on node 0.10, 0.12, 4, no allowed failures ([6684e8c8](https://github.com/mozilla/fxa-oauth-server/commit/6684e8c8)) - - -#### Features - -* **openid:** - * Add support for OIDC `login_hint` query param. ([200ce433](https://github.com/mozilla/fxa-oauth-server/commit/200ce433)) - * add initial OpenID Connect support ([93f87582](https://github.com/mozilla/fxa-oauth-server/commit/93f87582), closes [#362](https://github.com/mozilla/fxa-oauth-server/issues/362)) - - - -## 0.51.0 (2015-12-02) - - -#### Bug Fixes - -* **config:** option autoUpdateClients, will be disable in prod/stage ([802a0b22](https://github.com/mozilla/fxa-oauth-server/commit/802a0b22)) - - -#### Features - -* **token:** reject expired tokens ([4f519ca0](https://github.com/mozilla/fxa-oauth-server/commit/4f519ca0), closes [#365](https://github.com/mozilla/fxa-oauth-server/issues/365)) - - - -## 0.50.0 (2015-11-18) - - -#### Bug Fixes - -* **config:** update config to use getProperties ([c2ed6ebd](https://github.com/mozilla/fxa-oauth-server/commit/c2ed6ebd), closes [#349](https://github.com/mozilla/fxa-oauth-server/issues/349)) -* **db:** make schema.sql accuratley reflect latest patch state ([b17b0008](https://github.com/mozilla/fxa-oauth-server/commit/b17b0008)) -* **docs:** add git guidelines link ([a00167ce](https://github.com/mozilla/fxa-oauth-server/commit/a00167ce)) -* **travis:** remove broken validate shrinkwrap ([1729764f](https://github.com/mozilla/fxa-oauth-server/commit/1729764f)) - - -#### Features - -* **tokens:** allow using JWT grants from Service Clients ([55f88a9c](https://github.com/mozilla/fxa-oauth-server/commit/55f88a9c), closes [#328](https://github.com/mozilla/fxa-oauth-server/issues/328)) -* **verify:** add opt out parameter to verify endpoint ([e4c54ff6](https://github.com/mozilla/fxa-oauth-server/commit/e4c54ff6), closes [#358](https://github.com/mozilla/fxa-oauth-server/issues/358)) - - - -### 0.48.1 (2015-10-28) - - -#### Bug Fixes - -* **docs:** note that codes are single use ([6fe39f7b](https://github.com/mozilla/fxa-oauth-server/commit/6fe39f7b), closes [#214](https://github.com/mozilla/fxa-oauth-server/issues/214)) - - - -## 0.48.0 (2015-10-20) - - -#### Bug Fixes - -* **config:** remove 00000... from hashedSecrets ([8dcfd560](https://github.com/mozilla/fxa-oauth-server/commit/8dcfd560), closes [#339](https://github.com/mozilla/fxa-oauth-server/issues/339)) -* **dependencies:** move fxa-jwtool from dev-dependencies to dependencies ([79b0427a](https://github.com/mozilla/fxa-oauth-server/commit/79b0427a), closes [#345](https://github.com/mozilla/fxa-oauth-server/issues/345)) - - -#### Features - -* **tokens:** allow using JWT grants from Service Clients ([0a0e3034](https://github.com/mozilla/fxa-oauth-server/commit/0a0e3034), closes [#328](https://github.com/mozilla/fxa-oauth-server/issues/328)) - - - -## 0.47.0 (2015-10-07) - - -#### Bug Fixes - -* **deps:** update to mozlog 2.0.2 ([29342a92](http://github.com/mozilla/fxa-oauth-server/commit/29342a92445baa4ea45cc3f93c6a62e24c6d03d7), closes [#337](http://github.com/mozilla/fxa-oauth-server/issues/337)) - - - -## 0.46.0 (2015-09-23) - - -#### Features - -* **clients:** add notion of Service Clients in config ([8cfdffe8](http://github.com/mozilla/fxa-oauth-server/commit/8cfdffe8ba4e335e36949b6d3601b03ed0def2dd), closes [#327](http://github.com/mozilla/fxa-oauth-server/issues/327)) - - - -## 0.45.0 (2015-09-11) - - -#### Bug Fixes - -* **token:** disable expiration error ([c9547a8b](http://github.com/mozilla/fxa-oauth-server/commit/c9547a8b541b23956676b182df22b83c8a786e61)) -* **version:** use explicit path with git-config ([e0af8bcc](http://github.com/mozilla/fxa-oauth-server/commit/e0af8bccda69f478f286a86f4b11f6da485cc0f6)) - - -#### Features - -* **db:** remove clients.secret column ([0e39d1ee](http://github.com/mozilla/fxa-oauth-server/commit/0e39d1ee67818722d32f5ae0455ea56cf5d0cec1), closes [#323](http://github.com/mozilla/fxa-oauth-server/issues/323)) - - - -## 0.44.0 (2015-08-26) - - -#### Bug Fixes - -* **authorization:** allow empty scope with implicit grant ([1d6ac8e5](http://github.com/mozilla/fxa-oauth-server/commit/1d6ac8e55d28683072f448e022c33154bb4d7397), closes [#315](http://github.com/mozilla/fxa-oauth-server/issues/315)) -* **db:** don't change client database at startup; footgun ([8877f818](http://github.com/mozilla/fxa-oauth-server/commit/8877f818ec46a05a283e95e10fd8398756ad907c)) - - - -## 0.43.0 (2015-08-04) - - -#### Bug Fixes - -* **db:** we need to enforce only a minimum patch level (not {n,n+1}) ([e12f54d5](http://github.com/mozilla/fxa-oauth-server/commit/e12f54d5dc83c9f9595f7cc765ccc7e932361177)) -* **events:** require events to be configured in production ([1bef9e0a](http://github.com/mozilla/fxa-oauth-server/commit/1bef9e0aa26e15921d48205335a32565b255a6da)) -* **server:** exit if db patch level is wrong ([78d63829](http://github.com/mozilla/fxa-oauth-server/commit/78d6382980a8ce1e6adbcb2af5825f643cbcbccd)) - - -#### Breaking Changes - -* Server will fail to start up if `config.events` is not - set with values when in production. - ([1bef9e0a](http://github.com/mozilla/fxa-oauth-server/commit/1bef9e0aa26e15921d48205335a32565b255a6da)) - - - -## 0.42.0 (2015-07-22) - - -#### Bug Fixes - -* **config:** set expiration.accessToken default to 2 weeks ([7a4742de](http://github.com/mozilla/fxa-oauth-server/commit/7a4742dea75e59e66153273d77dd6cd5dd4b9d84)) -* **sql:** - * remove references to the `whitelisted` column; this is now the `trusted` column ([6b4d1ec3](http://github.com/mozilla/fxa-oauth-server/commit/6b4d1ec3f3fb72aa376ab28aa191198014f1bd84)) - * undo 155d2ce; for mysql-patcher fix up that database ([eb9f40d1](http://github.com/mozilla/fxa-oauth-server/commit/eb9f40d10389eb0de6a08b70089851789ac7f932)) - * fix the schema issue with the trailing comma ([069caeb4](http://github.com/mozilla/fxa-oauth-server/commit/069caeb4891c90c4d77649acded270b01785adca), closes [#299](http://github.com/mozilla/fxa-oauth-server/issues/299)) -* **tests:** sleep additional half second to adjust for mysql round of timestamp ([a02f5161](http://github.com/mozilla/fxa-oauth-server/commit/a02f5161d632e383ec55decc314a81668b600c82)) - - -#### Features - -* **api:** add ttl parameter to POST /authorization ([36087fe6](http://github.com/mozilla/fxa-oauth-server/commit/36087fe6dd4589d6451e007aa76edc4f0db2fcca)) - - - -## 0.41.0 (2015-07-07) - - -#### Bug Fixes - -* **api:** - * tolerate an empty client_secret in /destroy ([25a4d308](https://github.com/mozilla/fxa-oauth-server/commit/25a4d308)) - * accept and ignore client_secret param in /destroy ([c797ed23](https://github.com/mozilla/fxa-oauth-server/commit/c797ed23)) - * use invalidRequestParameter instead of invalidRedirect for invalid redirect acti ([55eff2dd](https://github.com/mozilla/fxa-oauth-server/commit/55eff2dd)) - * fail on invalid action parameters ([0c73ae79](https://github.com/mozilla/fxa-oauth-server/commit/0c73ae79)) -* **config:** update redirect_uri values to not be blank ([5267c62a](https://github.com/mozilla/fxa-oauth-server/commit/5267c62a)) - - -#### Features - -* **refresh_tokens:** add refresh_tokens to /token endpoint ([16e787f0](https://github.com/mozilla/fxa-oauth-server/commit/16e787f0), closes [#209](https://github.com/mozilla/fxa-oauth-server/issues/209)) - - - -## 0.39.0 (2015-06-10) - - -#### Bug Fixes - -* **api:** - * Correct the error codes changed in 2781b3a ([d0dba7c9](https://github.com/mozilla/fxa-oauth-server/commit/d0dba7c9)) - * Change InvalidAssertions error code to 401 ([2781b3a2](https://github.com/mozilla/fxa-oauth-server/commit/2781b3a2)) - * ensure /destroy endpoint returns an empty object in response body. ([6efd47d1](https://github.com/mozilla/fxa-oauth-server/commit/6efd47d1)) -* **clients:** fixes client registration to use payload.whitelisted ([83e145b0](https://github.com/mozilla/fxa-oauth-server/commit/83e145b0)) -* **docs:** - * Change Status Code for Invalid Assertion based ([780aaee3](https://github.com/mozilla/fxa-oauth-server/commit/780aaee3)) - * document keys and verification_redirect options ([ef8c47a5](https://github.com/mozilla/fxa-oauth-server/commit/ef8c47a5)) - * Update description of the `action` param to match latest reality. ([b475fcbc](https://github.com/mozilla/fxa-oauth-server/commit/b475fcbc)) -* **fatal-error:** Exit with non-zero exit code for fatal errors ([7c90ff08](https://github.com/mozilla/fxa-oauth-server/commit/7c90ff08), closes [#244](https://github.com/mozilla/fxa-oauth-server/issues/244)) - - -#### Features - -* **clients:** remove obsolete generate-client.js script ([62ab0adb](https://github.com/mozilla/fxa-oauth-server/commit/62ab0adb), closes [#231](https://github.com/mozilla/fxa-oauth-server/issues/231)) - - - -### 0.36.1 (2015-04-30) - - -#### Bug Fixes - -* **db:** remove db name from clients ([c7244393](https://github.com/mozilla/fxa-oauth-server/commit/c7244393)) - - -#### Features - -* **auth:** redirect to content-server oauth root by default ([34ad867c](https://github.com/mozilla/fxa-oauth-server/commit/34ad867c), closes [#245](https://github.com/mozilla/fxa-oauth-server/issues/245)) -* **clients:** - * add `terms_uri` and `privacy_uri` properties to clients. ([51ae9043](https://github.com/mozilla/fxa-oauth-server/commit/51ae9043)) - * report `trusted` property in GET /client/:id ([c58d237b](https://github.com/mozilla/fxa-oauth-server/commit/c58d237b)) -* **untrusted-clients:** restrict scopes that untrusted clients can request ([8fd228ad](https://github.com/mozilla/fxa-oauth-server/commit/8fd228ad), closes [#243](https://github.com/mozilla/fxa-oauth-server/issues/243)) - - - -## 0.36.0 (2015-04-27) - - -#### Features - -* **authorization:** exit early if assertion invalid returns first ([5a27ee61](https://github.com/mozilla/fxa-oauth-server/commit/5a27ee61)) -* **config:** - * add browserid pool maxSockets option ([0bb40ba1](https://github.com/mozilla/fxa-oauth-server/commit/0bb40ba1)) - * add mysql pool conectionLimit option ([ca220ae7](https://github.com/mozilla/fxa-oauth-server/commit/ca220ae7)) -* **developers:** adds support for oauth developers ([abe0e52a](https://github.com/mozilla/fxa-oauth-server/commit/abe0e52a)) -* **logging:** - * add log of time taken in authorization endpoint ([02ec0d20](https://github.com/mozilla/fxa-oauth-server/commit/02ec0d20)) - * add log when mysql pool enqueues ([461b5c19](https://github.com/mozilla/fxa-oauth-server/commit/461b5c19)) - - - -## 0.35.0 (2015-04-13) - - -#### Bug Fixes - -* **clients:** support client/client_id route via the internal server ([ce04da76](https://github.com/mozilla/fxa-oauth-server/commit/ce04da76)) - - - -## 0.33.0 (2015-03-16) - - -#### Bug Fixes - -* **clients:** fixes client endpoint for clients with no redirect_uri ([6d47110f](http://github.com/mozilla/fxa-oauth-server/commit/6d47110f5a3bbf4eb2e540c7ba09325e16dfec92), closes [#228](http://github.com/mozilla/fxa-oauth-server/issues/228)) -* **travis:** install libgmp3-dev so optionaldep bigint will be built for browserid-crypto ([a64cb183](http://github.com/mozilla/fxa-oauth-server/commit/a64cb183457e713fea092002cbecffd57961bb74)) - - -#### Features - -* **clients:** move client management api to a separate port ([07a61af2](http://github.com/mozilla/fxa-oauth-server/commit/07a61af2141e7fffc54d08a16acac48073103570)) - - - -### 0.30.3 (2015-02-20) - - -#### Bug Fixes - -* **clients:** update email validation ([92d4bfc3](http://github.com/mozilla/fxa-oauth-server/commit/92d4bfc30d5c9f17483f964195b8836bbb1e6557)) -* **db:** make the clients key mandatory in the config file ([ac7a39e8](http://github.com/mozilla/fxa-oauth-server/commit/ac7a39e8ad8506523a6721f87c9d70e70349aef7)) - - -#### Features - -* **docker:** Dockerfile and README update for basic docker development workflow ([342d87bb](http://github.com/mozilla/fxa-oauth-server/commit/342d87bb56d8204f83aad65449c02b2f2a233fec)) - - - -### 0.30.2 (2015-02-09) - - -#### Bug Fixes - -* **api:** remove stray payload restriction from authorization route ([e0d53682](http://github.com/mozilla/fxa-oauth-server/commit/e0d536821060dc044d875cbf5004c90c226cb09c)) -* **logging:** use route.path in debug message, not route.url ([7d9efc25](http://github.com/mozilla/fxa-oauth-server/commit/7d9efc253465c5d95d20f216a2ff6e42be82f1ca)) - - - -### 0.30.1 (2015-02-03) - - -#### Bug Fixes - -* **api:** - * allow application/x-form-urlencoded ([6cc91e28](http://github.com/mozilla/fxa-oauth-server/commit/6cc91e285fc51045a365dbacb3617ef29093dbc3)) - * reject requests with invalid parameters ([3b4fa244](http://github.com/mozilla/fxa-oauth-server/commit/3b4fa244454e5b33edf44d14a6da8be1d0fe98a6), closes [#210](http://github.com/mozilla/fxa-oauth-server/issues/210)) - - -#### Breaking Changes - -* If you're passing invalid parameters, stop it. - ([3b4fa244](http://github.com/mozilla/fxa-oauth-server/commit/3b4fa244454e5b33edf44d14a6da8be1d0fe98a6)) - - - -## 0.30.0 (2015-02-02) - - -#### Bug Fixes - -* **api:** reject requests with bad content-types ([26672287](http://github.com/mozilla/fxa-oauth-server/commit/26672287010658048afb5e83363319076799d976), closes [#199](http://github.com/mozilla/fxa-oauth-server/issues/199)) -* **clients:** fix server error when omitting optional fields in client registration ([80768c51](http://github.com/mozilla/fxa-oauth-server/commit/80768c51ea3cd1a26194f19951061992fd75bc1a)) - - -#### Features - -* **api:** - * add `auth_at` to token response schema. ([bc8454df](http://github.com/mozilla/fxa-oauth-server/commit/bc8454df90b1c4d8b94fc4bac993b76a8371432f)) - * allow destroying token without client_secret ([7b4d01ff](http://github.com/mozilla/fxa-oauth-server/commit/7b4d01ffc87dd3da74bf5eb7fc21ee07290090fd)) -* **db:** add basic migration infrastructure to mysql backend ([012e605c](http://github.com/mozilla/fxa-oauth-server/commit/012e605c501c5d135c16387ac6593931da73f589)) - - - -## 0.29.0 (2015-01-20) - - -#### Bug Fixes - -* **docs:** minor spelling fixes ([33ad1ec0](http://github.com/mozilla/fxa-oauth-server/commit/33ad1ec0b67116e3f9eb46b7ac9eb8f51548178b)) - - -#### Features - -* **api:** Add `action=force_auth` to GET /v1/authorization. ([33603bd2](http://github.com/mozilla/fxa-oauth-server/commit/33603bd2dc1b8a483563512f2f1f4729d64c0fc3)) - - - -### 0.26.2 (2014-11-20) - - -#### Bug Fixes - -* **logging:** use space-free tokens for mozlog ([11f73f9e](http://github.com/mozilla/fxa-oauth-server/commit/11f73f9e8e16324dba00822272f77a38828423f7)) - - - -### 0.26.1 (2014-11-13) - - -#### Features - -* **logging:** log details when generating code ([81933f70](http://github.com/mozilla/fxa-oauth-server/commit/81933f70a61c5783adc89dcea36f9f8213609e6a)) - - - -## 0.26.0 (2014-11-12) - - -#### Bug Fixes - -* **api:** set update to return an empty object ([6f334c66](http://github.com/mozilla/fxa-oauth-server/commit/6f334c668dc93f4ccba07c0aa14c316a5a433bca)) -* **error:** AppError uses Error.captureStackTrace ([2337f809](http://github.com/mozilla/fxa-oauth-server/commit/2337f809938ccef433667beb319be3b4de815da3), closes [#164](http://github.com/mozilla/fxa-oauth-server/issues/164)) - - -#### Features - -* **clients:** client registration apis ([1a80294d](http://github.com/mozilla/fxa-oauth-server/commit/1a80294dc3071b208d7573475b5be71c85e2aeb0)) -* **error:** add info property with link to docs ([681044c6](http://github.com/mozilla/fxa-oauth-server/commit/681044c6125b16b77cfa87a0cd7f5e2319f6bbab)) -* **logging:** - * add method, payload, and auth to summary ([df57e23c](http://github.com/mozilla/fxa-oauth-server/commit/df57e23cd737ae3a05a8b977ae377d00e570406b)) - * switch logging to mozlog ([ec0f5db1](http://github.com/mozilla/fxa-oauth-server/commit/ec0f5db1350b001176bbed84264cd1523a1d68b0), closes [#156](http://github.com/mozilla/fxa-oauth-server/issues/156)) -* **verify:** added 'client' to /verify response ([4c575516](http://github.com/mozilla/fxa-oauth-server/commit/4c5755164ceba608497cf36377746a6a3fbc41a8), closes [#149](http://github.com/mozilla/fxa-oauth-server/issues/149)) - - -#### Breaking Changes - -* both the config and the logging output has changed. - -Closes #156 - ([ec0f5db1](http://github.com/mozilla/fxa-oauth-server/commit/ec0f5db1350b001176bbed84264cd1523a1d68b0)) - - - -## 0.24.0 (2014-10-20) - - -#### Features - -* **server:** set HSTS header for 180 days ([d43accb9](http://github.com/mozilla/fxa-oauth-server/commit/d43accb9d7749a216840ba0cf51861becf974a81)) diff --git a/fxa-oauth-server/CONTRIBUTING.md b/fxa-oauth-server/CONTRIBUTING.md deleted file mode 100644 index 9c42af0..0000000 --- a/fxa-oauth-server/CONTRIBUTING.md +++ /dev/null @@ -1,81 +0,0 @@ -# Contributing - -Anyone is welcome to help with Firefox Accounts. Feel free to get in touch with other community members on IRC, the -mailing list or through issues here on GitHub. - -- IRC: `#fxa` on `irc.mozilla.org` -- Mailing list: -- and of course, [the issues list](https://github.com/mozilla/fxa-oauth-server/issues) - -## Bug Reports ## - -You can file issues here on GitHub. Please try to include as much information as you can and under what conditions -you saw the issue. - -## Sending Pull Requests ## - -Patches should be submitted as pull requests (PR). - -Before submitting a PR: -- Your code must run and pass all the automated tests before you submit your PR for review. "Work in progress" pull requests are allowed to be submitted, but should be clearly labeled as such and should not be merged until all tests pass and the code has been reviewed. - - Run `grunt eslint` to make sure your code passes linting. - - Run `npm test` to make sure all tests still pass. -- Your patch should include new tests that cover your changes. It is your and your reviewer's responsibility to ensure your patch includes adequate tests. - -When submitting a PR: -- You agree to license your code under the project's open source license ([MPL 2.0](/LICENSE)). -- Base your branch off the current `master` (see below for an example workflow). -- Add both your code and new tests if relevant. -- Run `grunt eslint` and `npm test` to make sure your code passes linting and tests. -- Please do not include merge commits in pull requests; include only commits with the new relevant code. -- Your commit message must follow the [commit guidelines](https://github.com/mozilla/fxa/blob/master/CONTRIBUTING.md#git-commit-guidelines). - -See the main [README.md](/README.md) for information on prerequisites, installing, running and testing. - -## Code Review ## - -This project is production Mozilla code and subject to our [engineering practices and quality standards](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Committing_Rules_and_Responsibilities). Every patch must be peer reviewed. This project is part of the [Firefox Accounts module](https://wiki.mozilla.org/Modules/Other#Firefox_Accounts), and your patch must be reviewed by one of the listed module owners or peers. - -## Example Workflow ## - -This is an example workflow to make it easier to submit Pull Requests. Imagine your username is `user1`: - -1. Fork this repository via the GitHub interface - -2. The clone the upstream (as origin) and add your own repo as a remote: - - ```sh - $ git clone https://github.com/mozilla/fxa-oauth-server.git - $ cd fxa-oauth-server - $ git remote add user1 git@github.com:user1/fxa-oauth-server.git -``` - -3. Create a branch for your fix/feature and make sure it's your currently checked-out branch: - - ```sh - $ git checkout -b add-new-feature -``` - -4. Add/fix code, add tests then commit and push this branch to your repo: - - ```sh - $ git add - $ git commit - $ git push user1 add-new-feature -``` - -5. From the GitHub interface for your repo, click the `Review Changes and Pull Request` which appears next to your new branch. - -6. Click `Send pull request`. - -### Keeping up to Date ### - -The main reason for creating a new branch for each feature or fix is so that you can track master correctly. If you need -to fetch the latest code for a new fix, try the following: - -```sh -$ git checkout master -$ git pull -``` - -Now you're ready to branch again for your new feature (from step 3 above). diff --git a/fxa-oauth-server/Dockerfile-build b/fxa-oauth-server/Dockerfile-build deleted file mode 100644 index 0419693..0000000 --- a/fxa-oauth-server/Dockerfile-build +++ /dev/null @@ -1,46 +0,0 @@ -FROM node:8-alpine AS builder - -RUN npm install -g npm@6 && rm -rf ~app/.npm /tmp/* - -RUN apk add --no-cache git && \ - apk add --repository http://dl-cdn.alpinelinux.org/alpine/v3.5/community/ --no-cache --virtual .build-deps git python make g++ - -RUN addgroup -g 10001 app && \ - adduser -D -G app -h /app -u 10001 app -WORKDIR /app - -# S3 bucket in Cloud Services prod IAM -ADD https://s3.amazonaws.com/dumb-init-dist/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init -RUN chmod +x /usr/local/bin/dumb-init -ENTRYPOINT ["/usr/local/bin/dumb-init", "--"] - -USER app - -COPY npm-shrinkwrap.json npm-shrinkwrap.json -COPY package.json package.json -COPY scripts/gen_keys.js scripts/gen_keys.js - -RUN npm install --production && rm -rf ~app/.npm /tmp/* - -COPY . /app - - -# Build final image by copying from builder -FROM node:8-alpine - -RUN npm install -g npm@6 && rm -rf ~app/.npm /tmp/* - -RUN apk add --no-cache git - -RUN addgroup -g 10001 app && \ - adduser -D -G app -h /app -u 10001 app -WORKDIR /app - -# S3 bucket in Cloud Services prod IAM -ADD https://s3.amazonaws.com/dumb-init-dist/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init -RUN chmod +x /usr/local/bin/dumb-init -ENTRYPOINT ["/usr/local/bin/dumb-init", "--"] - -USER app - -COPY --from=builder --chown=app /app/ /app/ diff --git a/fxa-oauth-server/Dockerfile-test b/fxa-oauth-server/Dockerfile-test deleted file mode 100644 index ba2c470..0000000 --- a/fxa-oauth-server/Dockerfile-test +++ /dev/null @@ -1,5 +0,0 @@ -FROM fxa-oauth-server:build -USER root -RUN apk add --repository http://dl-cdn.alpinelinux.org/alpine/v3.5/community/ --no-cache --virtual .build-deps git python make g++ -USER app -RUN npm install diff --git a/fxa-oauth-server/Gruntfile.js b/fxa-oauth-server/Gruntfile.js deleted file mode 100644 index beaf2bb..0000000 --- a/fxa-oauth-server/Gruntfile.js +++ /dev/null @@ -1,25 +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/. */ - -module.exports = function (grunt) { - 'use strict'; - - // show elapsed time at the end - require('time-grunt')(grunt); - // load all grunt tasks - require('load-grunt-tasks')(grunt); - - grunt.initConfig({ - pkg: grunt.file.readJSON('./package.json'), - // .js files for ESLint, JSHint, JSCS, etc. - mainJsFiles: '{,lib/**/,scripts/**/,test/**/,tasks/**/,bin/**/}*.js' - }); - - grunt.loadTasks('grunttasks'); - - grunt.registerTask('default', [ - 'lint', - 'copyright' - ]); -}; diff --git a/fxa-oauth-server/LICENSE b/fxa-oauth-server/LICENSE deleted file mode 100644 index 14e2f77..0000000 --- a/fxa-oauth-server/LICENSE +++ /dev/null @@ -1,373 +0,0 @@ -Mozilla Public License Version 2.0 -================================== - -1. Definitions --------------- - -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. - -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - means Source Code Form to which the initial Contributor has attached - the notice in Exhibit A, the Executable Form of such Source Code - Form, and Modifications of such Source Code Form, in each case - including portions thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - means a work that combines Covered Software with other material, in - a separate file or files, that is not Covered Software. - -1.8. "License" - means this document. - -1.9. "Licensable" - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, any and - all of the rights conveyed by this License. - -1.10. "Modifications" - means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the - License, by the making, using, selling, offering for sale, having - made, import, or transfer of either its Contributions or its - Contributor Version. - -1.12. "Secondary License" - means either the GNU General Public License, Version 2.0, the GNU - Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those - licenses. - -1.13. "Source Code Form" - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that - controls, is controlled by, or is under common control with You. For - purposes of this definition, "control" means (a) the power, direct - or indirect, to cause the direction or management of such entity, - whether by contract or otherwise, or (b) ownership of more than - fifty percent (50%) of the outstanding shares or beneficial - ownership of such entity. - -2. License Grants and Conditions --------------------------------- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer - for sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of - its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities -------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more - than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ---------------------------------------------------- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ----------------- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ---------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - 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/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. diff --git a/fxa-oauth-server/README.md b/fxa-oauth-server/README.md deleted file mode 100644 index ed3598d..0000000 --- a/fxa-oauth-server/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Firefox Accounts OAuth Server - -[![Build Status](https://travis-ci.org/mozilla/fxa-oauth-server.svg?branch=master)](https://travis-ci.org/mozilla/fxa-oauth-server) -[![CircleCI](https://circleci.com/gh/mozilla/fxa-oauth-server.svg?style=svg)](https://circleci.com/gh/mozilla/fxa-oauth-server) - -Implementation of OAuth for use by Firefox Accounts - -[API docs](./docs/api.md) - -[Design document](https://github.com/mozilla/fxa-oauth-server/wiki/oauth-design) - -[MDN docs](https://developer.mozilla.org/en-US/Firefox_Accounts) - -## Quick Start - -Clone the repository, run `npm install` and `npm start`. - -To get a full development setup running use [fxa-local-dev](https://github.com/mozilla/fxa-local-dev). fxa-local-dev is the preferred way of contributing to Firefox Accounts. - -## License - -MPL 2.0 diff --git a/fxa-oauth-server/bin/internal.js b/fxa-oauth-server/bin/internal.js deleted file mode 100644 index 01beb27..0000000 --- a/fxa-oauth-server/bin/internal.js +++ /dev/null @@ -1,23 +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/. */ - -const config = require('../lib/config').getProperties(); -const db = require('../lib/db'); -const logger = require('../lib/logging')('bin.internal'); -const serverPromise = require('../lib/server/internal').create(); - -logger.debug('config', config); -db.ping().done(function() { - let server; - - serverPromise.then((s) => { - server = s; - return server.start(); - }).then(() => { - logger.info('listening', server.info.uri); - }); -}, function(err) { - logger.critical('db.ping', err); - process.exit(1); -}); diff --git a/fxa-oauth-server/bin/purge_expired_tokens.js b/fxa-oauth-server/bin/purge_expired_tokens.js deleted file mode 100644 index 6b75697..0000000 --- a/fxa-oauth-server/bin/purge_expired_tokens.js +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env node - -/* 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/. - * - * This is a command line tool that can be used to purge expired tokens - * from the OAuth database. It requires you specify a pocket client id - * before running. Currently, access tokens created from pocket should - * not be deleted even if expired. - * - * Example usage: - * - * node purge_expired_tokens.js --config dev --pocket-id dcdb5ae7add825d2 --token-count 10000 --delay-seconds 1 - * - * or, for multiple pocket ids: - * - * node purge_expired_tokens.js --config dev --pocket-id dcdb5ae7add825d2,678f75ae1c0f8002 --token-count 10000 --delay-seconds 1 - * - * */ - -const config = require('../lib/config'); -const package = require('../package.json'); -const program = require('commander'); - -// Don't bother updating the clients table. -config.set('db.autoUpdateClients', false); - -program - .version(package.version) - .option('-c, --config [config]', 'Configuration to use. Ex. dev') - .option('-p, --pocket-id ', 'Pocket Client Ids. These tokens will not be purged. (CSV)') - .option('-t, --token-count ', 'Total number of tokens to delete.') - .option('-d, --delay-seconds ', 'Delay (seconds) between each deletion round. (Default: 1 second)') - .option('-I, --by-id', 'Delete tokens by selecting, then deleting by primary id (Default: false)') - .option('-D, --delete-batch-size ', 'Number of tokens to delete in each deletion round. (Default: 200)') - .parse(process.argv); - -if (! program.config) { - program.config = 'dev'; -} - -process.env.NODE_ENV = program.config; - -const db = require('../lib/db'); -const logger = require('../lib/logging')('bin.purge_expired_tokens'); - -if (! program.pocketId) { - logger.error('invalid', { message: 'Required pocket client id!' }); - process.exit(1); -} - -const numberOfTokens = parseInt(program.tokenCount) || 200; -const delaySeconds = Number(program.delaySeconds) || 1; // Default 1 seconds -const deleteBatchSize = Number(program.deleteBatchSize) || 200; // Default 200 -// There may be more than one pocketId, so treat this as a comma-separated list. -const ignorePocketClientId = program.pocketId.toLowerCase().split(/\s*,\s*/g); - -db.ping().done(() => { - // Only mysql impl supports token deletion at the moment - if (! db.purgeExpiredTokens) { - const message = ('Unable to purge expired tokens, only available ' + - 'when using config with mysql database.'); - logger.info('skipping', { message: message }); - return; - } - - logger.info('deleting', { - numberOfTokens: numberOfTokens, - delaySeconds: delaySeconds, - deleteBatchSize: deleteBatchSize, - ignorePocketClientId: ignorePocketClientId - }); - - // To reduce the risk of deleting pocket tokens, ensure that the pocket-id - // passed in belongs to a client. - const purgeMethod = program.byId ? db.purgeExpiredTokensById : db.purgeExpiredTokens; - return purgeMethod(numberOfTokens, - delaySeconds, - ignorePocketClientId, - deleteBatchSize) - .then(() => { - logger.info('completed'); - process.exit(0); - }) - .catch((err) => { - logger.error('error', err); - process.exit(1); - }); -}, (err) => { - logger.critical('db.ping', err); - process.exit(1); -}); - -process.on('uncaughtException', (err) => { - logger.error('error', err); - process.exit(2); -}); diff --git a/fxa-oauth-server/bin/server.js b/fxa-oauth-server/bin/server.js deleted file mode 100644 index c40b6c9..0000000 --- a/fxa-oauth-server/bin/server.js +++ /dev/null @@ -1,30 +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/. */ - -const config = require('../lib/config').getProperties(); -const db = require('../lib/db'); -const logger = require('../lib/logging')('bin.server'); -const serverPromise = require('../lib/server').create(); -const events = require('../lib/events'); - -logger.debug('config', config); -db.ping().done(function() { - let server; - - serverPromise.then((s) => { - server = s; - return server.start(); - }).then(() => { - logger.info('listening', server.info.uri); - events.start(); - }); - -}, function(err) { - logger.critical('db.ping', err); - process.exit(1); -}); - -process.on('uncaughtException', function() { - process.exit(2); -}); diff --git a/fxa-oauth-server/config/dev.json b/fxa-oauth-server/config/dev.json deleted file mode 100644 index 7b79882..0000000 --- a/fxa-oauth-server/config/dev.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "browserid": { - "issuer": "127.0.0.1:9000", - "verificationUrl": "http://127.0.0.1:5050/v2" - }, - "contentUrl": "http://127.0.0.1:3030/oauth/", - "clients": [ - { - "id": "dcdb5ae7add825d2", - "hashedSecret": "289a885946ee316844d9ffd0d725ee714901548a1e6507f1a40fb3c2ae0c99f1", - "name": "123Done", - "imageUri": "https://mozorg.cdn.mozilla.net/media/img/firefox/new/header-firefox.png", - "redirectUri": "http://127.0.0.1:8080/api/oauth", - "trusted": true, - "canGrant": false - }, - { - "id": "38a6b9b3a65a1871", - "hashedSecret": "289a885946ee316844d9ffd0d725ee714901548a1e6507f1a40fb3c2ae0c99f1", - "name": "123Done PKCE", - "imageUri": "https://mozorg.cdn.mozilla.net/media/img/firefox/new/header-firefox.png", - "redirectUri": "http://127.0.0.1:8080/?oauth_pkce_redirect=1", - "trusted": true, - "canGrant": false, - "publicClient": true - }, - { - "id": "22d74070a481bc73", - "name": "Test Client iOS", - "hashedSecret": "88716ed2927c96cdc0fb7efe57d5f124fb4161066c1ff7f4263069822256ec66", - "redirectUri": "com.mozilla.sandvich:/oauth2redirect/fxa-provider", - "imageUri": "", - "publicClient": true, - "canGrant": false, - "termsUri": "", - "privacyUri": "", - "trusted": true, - "allowedScopes": "https://identity.mozilla.com/apps/oldsync" - }, - { - "id": "325b4083e32fe8e7", - "hashedSecret": "ded3c396f28123f3fe6b152784e8eab7357c6806cb5175805602a2cd67f85080", - "name": "321Done Untrusted", - "imageUri": "https://mozorg.cdn.mozilla.net/media/img/firefox/new/header-firefox.png", - "redirectUri": "http://127.0.0.1:10139/api/oauth", - "trusted": false, - "canGrant": false - }, - { - "id": "7f368c6886429f19", - "name": "Firefox Notes Android Dev", - "hashedSecret": "9c716ed2927c96cdc0fb7efe57d5f124fb4161066c1ff7f4263069822256ec3f", - "redirectUri": "https://mozilla.github.io/notes/fxa/android-redirect.html", - "imageUri": "", - "canGrant": false, - "termsUri": "", - "privacyUri": "", - "trusted": true, - "allowedScopes": "https://identity.mozilla.com/apps/notes", - "publicClient": true - }, - { - "id": "c6d74070a481bc10", - "name": "Firefox Notes Dev", - "hashedSecret": "9c716ed2927c96cdc0fb7efe57d5f124fb4161066c1ff7f4263069822256ec3f", - "redirectUri": "https://dee85c67bd72f3de1f0a0fb62a8fe9b9b1a166d7.extensions.allizom.org/", - "imageUri": "", - "canGrant": false, - "termsUri": "", - "privacyUri": "", - "trusted": true, - "allowedScopes": "https://identity.mozilla.com/apps/notes", - "publicClient": true - }, - { - "id": "98e6508e88680e1a", - "hashedSecret": "ba5cfb370fd782f7eae1807443ab816288c101a54c0d80a09063273c86d3c435", - "name": "Firefox Accounts Settings", - "imageUri": "https://example2.domain/logo", - "redirectUri": "https://example2.domain/return?foo=bar", - "trusted": true, - "canGrant": true - }, - { - "name": "FxA OAuth Console", - "redirectUri": "http://127.0.0.1:10137/oauth/redirect", - "imageUri": "http://127.0.0.1:10137/assets/firefox.png", - "id": "24bdbfa45cd300c5", - "hashedSecret": "dfe56d5c816d6b7493618f6a1567cfed4aa9c25f85d59c6804631c48774ba545", - "trusted": true, - "canGrant": false - }, - { - "name": "Firefox Hello DEV", - "redirectUri": "urn:ietf:wg:oauth:2.0:fx:webchannel", - "imageUri": "https://example2.domain/return?foo=bar", - "id": "263ceaa5546dce83", - "hashedSecret": "3c32099123471ffb80a7553558e6a6e8589da2235a4e081fe9d76648aa25d050", - "trusted": true, - "canGrant": false - }, - { - "name": "Firefox", - "id": "5882386c6d801776", - "hashedSecret": "71b5283536f1f1c331eca2f75c58a5947d7a7ac54164eadb4b33a889afe89fbf", - "imageUri": "", - "redirectUri": "urn:ietf:wg:oauth:2.0:oob", - "trusted": true, - "canGrant": true - }, - { - "name": "Fennec", - "id": "3332a18d142636cb", - "hashedSecret": "99ee06fa07919c5208694d34d761fa95ee5a0bbbaad3f3ebaa6042b04a6bdec1", - "imageUri": "", - "redirectUri": "urn:ietf:wg:oauth:2.0:oob", - "trusted": true, - "canGrant": true - }, - { - "name": "Firefox OS", - "id": "d0eea24a1d613eeb", - "hashedSecret": "5a1f1deb84752118a8b8020b7f2c6f9aea76a0e968bead2478ebc309704e29ee", - "imageUri": "", - "redirectUri": "urn:ietf:wg:oauth:2.0:oob", - "trusted": true, - "canGrant": true - }, - { - "name": "Firefox Accounts", - "id": "ea3ca969f8c6bb0d", - "hashedSecret": "744559ea3d0f69eb5185cbd5b176a38e09d013c6459dbb3cbc25b4c5b165d33f", - "imageUri": "", - "redirectUri": "urn:ietf:wg:oauth:2.0:oob", - "trusted": true, - "canGrant": true - } - ], - "localRedirects": true, - "logging": { - "level": "ALL", - "fmt": "pretty" - }, - "openid": { - "issuer": "http://127.0.0.1:3030", - "keyFile": "../config/key.json", - "key": {} - }, - "allowHttpRedirects": true, - "scopes": [ - { - "scope": "https://identity.mozilla.com/apps/notes", - "hasScopedKeys": true - }, - { - "scope": "https://identity.mozilla.com/apps/oldsync", - "hasScopedKeys": true - } - ] -} diff --git a/fxa-oauth-server/config/test.json b/fxa-oauth-server/config/test.json deleted file mode 100644 index 1018467..0000000 --- a/fxa-oauth-server/config/test.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "clients": [ - { - "id": "dcdb5ae7add825d2", - "hashedSecret": "289a885946ee316844d9ffd0d725ee714901548a1e6507f1a40fb3c2ae0c99f1", - "hashedSecretPrevious": "0726282857047586fb4edc335b5492ef1e4a0d95d3f1114627bb89b4e57cf6e1", - "name": "Mocha", - "imageUri": "https://example.domain/logo", - "redirectUri": "https://example.domain/return?foo=bar", - "trusted": true, - "canGrant": false - }, - { - "id": "98e6508e88680e1a", - "hashedSecret": "0000000000000000000000000000000000000000000000000000000000000000", - "name": "Admin", - "imageUri": "https://example2.domain/logo", - "redirectUri": "https://example2.domain/redirect", - "trusted": true, - "canGrant": true, - "publicClient": true - }, - { - "id": "98e6508e88680e1b", - "hashedSecret": "ba5cfb370fd782f7eae1807443ab816288c101a54c0d80a09063273c86d3c435", - "name": "URN", - "imageUri": "https://example2.domain/logo", - "redirectUri": "urn:ietf:wg:oauth:2.0:fx:webchannel", - "trusted": true, - "canGrant": false - }, - { - "name": "NoRedirectUri", - "id": "ea3ca969f8c6bb0d", - "hashedSecret": "d962cdf34a33ab26f7a6b900d0e1028f182d8e4811cb9b5ac4f20275525c8f54", - "imageUri": "", - "redirectUri": "", - "trusted": true, - "canGrant": false - }, - { - "name": "Untrusted", - "id": "ea3ca969f8c6bb0e", - "hashedSecret": "ec62e3281e3b56e702fe7e82ca7b1fa59d6c2a6766d6d28cccbf8bfa8d5fc8a8", - "imageUri": "", - "redirectUri": "https://example.domain/return?foo=bar", - "trusted": false, - "canGrant": false - }, - { - "id": "38a6b9b3a65a1871", - "hashedSecret": "289a885946ee316844d9ffd0d725ee714901548a1e6507f1a40fb3c2ae0c99f1", - "name": "Public Client PKCE", - "imageUri": "https://mozorg.cdn.mozilla.net/media/img/firefox/new/header-firefox.png", - "redirectUri": "https://example.domain/return?foo=bar", - "trusted": true, - "allowedScopes": "kv", - "canGrant": false, - "publicClient": true - }, - { - "id": "aaa6b9b3a65a1871", - "hashedSecret": "289a885946ee316844d9ffd0d725ee714901548a1e6507f1a40fb3c2ae0c99f1", - "name": "Scoped Key Client", - "imageUri": "https://mozorg.cdn.mozilla.net/media/img/firefox/new/header-firefox.png", - "redirectUri": "https://example.domain/return?foo=bar", - "trusted": true, - "allowedScopes": "https://identity.mozilla.com/apps/sample-scope-can-scope-key https://identity.mozilla.com/apps/sample-scope kv https://identity.mozilla.com/apps/another-can-scope-key", - "canGrant": false, - "publicClient": false - } - ], - "logging": { - "level": "error", - "fmt": "pretty" - }, - "openid": { - "keyFile": "../config/key.json", - "oldKeyFile": "../config/oldKey.json", - "key": {}, - "oldKey": {} - }, - "serviceClients": [ - { - "id": "d23dbf62b82eb04e", - "name": "Test Service Client", - "scope": "profile", - "jku": "http://127.0.0.1:9019/.well-known/public-keys" - } - ], - "allowHttpRedirects": true, - "scopes": [ - { - "scope": "https://identity.mozilla.com/apps/sample-scope", - "hasScopedKeys": false - }, - { - "scope": "https://identity.mozilla.com/apps/sample-scope-can-scope-key", - "hasScopedKeys": true - }, - { - "scope": "https://identity.mozilla.com/apps/another-can-scope-key", - "hasScopedKeys": true - } - ] -} diff --git a/fxa-oauth-server/docs/api.md b/fxa-oauth-server/docs/api.md deleted file mode 100644 index df543fa..0000000 --- a/fxa-oauth-server/docs/api.md +++ /dev/null @@ -1,669 +0,0 @@ -# Firefox Accounts OAuth Server API - -## Overview - -### URL Structure - -``` -https:///v1/ -``` - -Note that: - -- All API access must be over HTTPS. -- The URL embeds a version identifier "v1"; future revisions of this API may introduce new version numbers. -- The base URL of the server may be configured on a per-client basis. - -### Errors - -Invalid requests will return 4XX responses. Internal failures will return 5XX. Both will include JSON responses describing the error. - -**Example error:** - -```js -{ - "code": 400, // matches the HTTP status code - "errno": 101, // stable application-level error number - "error": "Bad Request", // string description of error type - "message": "Unknown client" -} -``` - -The currently-defined error responses are: - -| status code | errno | description | -|:-----------:|:-----:|-------------| -| 400 | 101 | unknown client id | -| 400 | 102 | incorrect client secret | -| 400 | 103 | `redirect_uri` doesn't match registered value | -| 401 | 104 | invalid fxa assertion | -| 400 | 105 | unknown code | -| 400 | 106 | incorrect code | -| 400 | 107 | expired code | -| 400 | 108 | invalid token | -| 400 | 109 | invalid request parameter | -| 400 | 110 | invalid response_type | -| 401 | 111 | unauthorized | -| 403 | 112 | forbidden | -| 415 | 113 | invalid content type | -| 400 | 114 | invalid scopes | -| 400 | 115 | expired token | -| 500 | 999 | internal server error | - -## API Endpoints - - -- [GET /v1/authorization][redirect] -- [GET /v1/jwks][jwks] -- [POST /v1/authorization][authorization] -- [POST /v1/token][token] -- [POST /v1/destroy][delete] -- Clients - - [GET /v1/client/:id][client] - - [GET /v1/clients][clients] - - [POST /v1/client][register] - - [POST /v1/client/:id][client-update] - - [DELETE /v1/client/:id][client-delete] -- Developers - - [POST /v1/developer/activate][developer-activate] -- [POST /v1/verify][verify] -- [POST /v1/key-data][key-data] -- [GET /v1/client-tokens][client-tokens] -- [DELETE /v1/client-tokens/:id][client-tokens-delete] - -### GET /v1/client/:id - -This endpoint is for the fxa-content-server to retrieve information -about a client to show in its user interface. - -#### Request Parameters - -- `id`: The `client_id` of a client asking for permission. - -**Example:** - -```sh -curl -v "https://oauth.accounts.firefox.com/v1/client/5901bd09376fadaa" -``` - -#### Response - -A valid 200 response will be a JSON blob with the following properties: - -- `name`: A string name of the client. -- `image_uri`: A url to a logo or image that represents the client. -- `redirect_uri`: The url registered to redirect to after successful oauth. -- `trusted`: Whether the client is a trusted internal application. - -**Example:** - -```json -{ - "name": "Where's My Fox", - "image_uri": "https://mozilla.org/firefox.png", - "redirect_uri": "https://wheres.my.firefox.com/oauth", - "trusted": true -} -``` - -### GET /v1/clients - -Get a list of all registered clients. - -**Required scope:** `oauth` - -#### Request - -**Example:** - - -```sh -curl -v \ --H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" \ -"https://oauth.accounts.firefox.com/v1/clients" -``` - -#### Response - -A valid 200 response will be a JSON object with a property of `clients`, -which contains an array of client objects. - -**Example:** - -```json -{ - "clients": [ - { - "id": "5901bd09376fadaa", - "name": "Example", - "redirect_uri": "https://ex.am.ple/path", - "image_uri": "https://ex.am.ple/logo.png", - "can_grant": false, - "trusted": false - } - ] -} -``` - -### POST /v1/client - -Register a new client (FxA relier). - -**Required scope:** `oauth` - -#### Request Parameters - -- `name`: The name of the client. -- `redirect_uri`: The URI to redirect to after logging in. -- `image_uri`: A URI to an image to show to a user when logging in. -- `trusted`: Whether the client is a trusted internal application. -- `can_grant`: A client needs permission to get implicit grants. - -**Example:** - -```sh -curl -v \ --X POST \ --H "Content-Type: application/json" \ --H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" \ -"https://oauth.accounts.firefox.com/v1/client" \ --d '{ - "name": "Example", - "redirect_uri": "https://ex.am.ple/path", - "image_uri": "https://ex.am.ple/logo.png", - "trusted": false, - "can_grant": false -}' -``` - -#### Response - -A valid 201 response will be a JSON blob with the following properties: - -- `client_id`: The generated id for this client. -- `client_secret`: The generated secret for this client. *NOTE: This is - the only time you can get the secret, because we only keep a hashed - version.* -- `name`: A string name of the client. -- `image_uri`: A url to a logo or image that represents the client. -- `redirect_uri`: The url registered to redirect to after successful oauth. -- `can_grant`: If the client can get implicit grants. -- `trusted`: Whether the client is a trusted internal application. - -**Example:** - -```json -{ - "client_id": "5901bd09376fadaa", - "client_secret": "4ab433e31ef3a7cf7c20590f047987922b5c9ceb1faff56f0f8164df053dd94c", - "name": "Example", - "redirect_uri": "https://ex.am.ple/path", - "image_uri": "https://ex.am.ple/logo.png", - "can_grant": false, - "trusted": false -} -``` - -### POST /v1/client/:id - -Update the details of a client. Any parameter not included in the -request will stay unchanged. - -**Required scope:** `oauth` - -#### Request Parameters - -- `name`: The name of the client. -- `redirect_uri`: The URI to redirect to after logging in. -- `image_uri`: A URI to an image to show to a user when logging in. -- `trusted`: Whether the client is a trusted internal application. -- `can_grant`: A client needs permission to get implicit grants. - -**Example:** - -```sh -curl -v \ --X POST \ --H "Content-Type: application/json" \ --H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" \ -"https://oauth.accounts.firefox.com/v1/client/5901bd09376fadaa" \ --d '{ - "name": "Example2", - "redirect_uri": "https://ex.am.ple/path/2", - "image_uri": "https://ex.am.ple/logo2.png", -}' -``` - -#### Response - -A valid response will have a 200 status code and empty object `{}`. - -### DELETE /v1/client/:id - -Delete a client. It will be no more. Zilch. Nada. Nuked from orbit. - -**Required scope:** `oauth` - -#### Request Parameters - -**Example:** - -```sh -curl -v \ --X DELETE \ --H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" \ -"https://oauth.accounts.firefox.com/v1/client/5901bd09376fadaa" -``` - -#### Response - -A valid response will have a 204 response code and an empty body. - -### POST /v1/developer/activate - -Register an oauth developer. - -**Required scope:** `oauth` - -#### Request Parameters - -- None - -#### Response - -A valid response will have a 200 status code and a developer object: -``` -{"developerId":"f5b176ab5be5928d01d4bb0a6c182994","email":"d91c30a8@mozilla.com","createdAt":"2015-03-23T01:22:59.000Z"} -``` - -### GET /v1/authorization - -This endpoint starts the OAuth flow. A client redirects the user agent -to this url. This endpoint will then redirect to the appropriate -content-server page. - -#### Request Parameters - -- `client_id`: The id returned from client registration. -- `state`: A value that will be returned to the client as-is upon redirection, so that clients can verify the redirect is authentic. -- `redirect_uri`: Optional. If supplied, a string URL of where to redirect afterwards. Must match URL from registration. -- `scope`: Optional. A space-separated list of scopes that the user has authorized. This could be pruned by the user at the confirmation dialog. If this includes the scope `openid`, this will be an OpenID Connect authentication request. -- `access_type`: Optional. If provided, should be `online` or `offline`. `offline` will result in a refresh_token being provided, so that the access_token can be refreshed after it expires. -- `action`: Optional. If provided, should be `email`, `signup`, `signin`, or `force_auth`. Send to improve the user experience. - - If unspecified then Firefox Accounts will try choose intelligently between `signin` and `signup` based on the user's browser state. - - `email` triggers the email-first flow, which uses the email address to determine whether to display signup or signin. This is becoming the **preferred** action and is slowly replacing `signin` and `signup`. - - `signin` triggers the signin flow. (will become depricated and replaced by `email`) - - `signup` triggers the signup flow. (will become depricated and replaced by `email`) - - `force_auth` requires the user to sign in using the address specified in `email`. -- `email`: Optional if `action` is `email`, `signup` or `signin`. Required if `action` - is `force_auth`. - - if `action` is `email`, the email address will be used to determine whether to display the signup or signin form, but the user is free to change it. - - If `action` is `signup` or `signin`, the email address will be pre-filled into the account form, but the user is free to change it. - - If `action` is `signin`, the literal string `blank` will force the user to enter an email address and the last signed in email address will be ignored. - - If `action` is `signin` and no email address is specified, the last - signed in email address will be used as the default. - - If `action` is `force_auth`, the user is unable to modify the email - address and is unable to sign up if the address is not registered. - -**Example:** - -```sh -curl -v "https://oauth.accounts.firefox.com/v1/authorization?client_id=5901bd09376fadaa&state=1234&scope=profile:email&action=signup" -``` - -### POST /v1/authorization - -This endpoint should be used by the fxa-content-server, requesting that -we supply a short-lived code (currently 15 minutes) that will be sent -back to the client. This code will be traded for a token at the -[token][] endpoint. - -#### Request Parameters - -- `client_id`: The id returned from client registration. -- `assertion`: A FxA assertion for the signed-in user. -- `state`: A value that will be returned to the client as-is upon redirection, so that clients can verify the redirect is authentic. -- `response_type`: Optional. If supplied, must be either `code` or `token`. `code` is the default. `token` means the implicit grant is desired, and requires that the client have special permission to do so. -- `ttl`: Optional if `response_type=token`, forbidden if `response_type=code`. Indicates the requested lifespan in seconds for the implicit grant token. The value is subject to an internal maximum limit, so clients must check the `expires_in` result property for the actual TTL. -- `redirect_uri`: Optional. If supplied, a string URL of where to redirect afterwards. Must match URL from registration. -- `scope`: Optional. A string-separated list of scopes that the user has authorized. This could be pruned by the user at the confirmation dialog. -- `access_type`: Optional. A value of `offline` will generate a refresh token along with the access token. -- `code_challenge_method`: Required if using [PKCE](pkce.md). Must be `S256`, no other value is accepted. -- `code_challenge`: Required if using [PKCE](pkce.md). A minimum length of 43 characters and a maximum length of 128 characters string, encoded as `BASE64URL`. -- `keys_jwe`: Optional. A JWE bundle to be returned to the client when it redeems the authorization code. - - -**Example:** - -```sh -curl -v \ --X POST \ --H "Content-Type: application/json" \ -"https://oauth.accounts.firefox.com/v1/authorization" \ --d '{ - "client_id": "5901bd09376fadaa", - "assertion": "", - "state": "1234", - "scope": "profile:email" -}' -``` - -#### Response - -A valid request will return a 200 response, with JSON containing the `redirect` to follow. It will include the following query parameters: - -- `code`: A string that the client will trade with the [token][] endpoint. Codes have a configurable expiration value, default is 15 minutes. Codes are single use only. -- `state`: The same value as was passed as a request parameter. - -**Example:** - -```json -{ - "redirect": "https://example.domain/path?foo=bar&code=4ab433e31ef3a7cf7c20590f047987922b5c9ceb1faff56f0f8164df053dd94c&state=1234" -} -``` - -##### Implicit Grant - -If requesting an implicit grant (token), the response will match the -[/v1/token][token] response. - - -### POST /v1/token - -After having received a [code][authorization], the client sends that code (most -likely a server-side request) to this endpoint, to receive a -longer-lived token that can be used to access attached services for a -particular user. - -#### Request Parameters - -- `ttl`: (optional) Seconds that this access_token should be valid. - - The default and maximum value is 2 weeks. -- `grant_type`: Either `authorization_code`, `refresh_token`, or `urn:ietf:params:oauth:grant-type:jwt-bearer`. - - If `authorization_code`: - - `client_id`: The id returned from client registration. - - `client_secret`: The secret returned from client registration. - - `code`: A string that was received from the [authorization][] endpoint. - - If `refresh_token`: - - `client_id`: The id returned from client registration. - - `client_secret`: The secret returned from client registration. - This must not be set if the client is a public (PKCE) client. - - `refresh_token`: A string that received from the [token][] - endpoint specifically as a refresh token. - - `scope`: (optional) A subset of scopes provided to this - refresh_token originally, to receive an access_token with less - permissions. - - If `urn:ietf:params:oauth:grant-type:jwt-bearer`: - - `assertion`: A signed JWT assertion. See [Service - Clients][] for more. - - if client is type `publicClient:true` and `authorization_code`: - - `code_verifier`: Required if using [PKCE](pkce.md). - - - -**Example:** - -```sh -curl -v \ --X POST \ --H "Content-Type: application/json" \ -"https://oauth.accounts.firefox.com/v1/token" \ --d '{ - "client_id": "5901bd09376fadaa", - "client_secret": "20c6882ef864d75ad1587c38f9d733c80751d2cbc8614e30202dc3d1d25301ff", - "ttl": 3600, - "grant_type": "authorization_code", - "code": "4ab433e31ef3a7cf7c20590f047987922b5c9ceb1faff56f0f8164df053dd94c" -}' -``` - -#### Response - -A valid request will return a JSON response with these properties: - -- `access_token`: A string that can be used for authorized requests to service providers. -- `scope`: A string of space-separated permissions that this token has. May differ from requested scopes, since user can deny permissions. -- `refresh_token`: (Optional) A refresh token to fetch a new access token when this one expires. Only will be present if `grant_type=authorization_code` and the original authorization request included `access_type=offline`. -- `expires_in`: **Seconds** until this access token will no longer be valid. -- `token_type`: A string representing the token type. Currently will always be "bearer". -- `auth_at`: An integer giving the time at which the user authenticated to the Firefox Accounts server when generating this token, as a UTC unix timestamp (i.e. **seconds since epoch**). -- `id_token`: (Optional) If the authorization was requested with `openid` scope, then this property will contain the OpenID Connect ID Token. -- `keys_jwe`: (Optional) Returns the JWE bundle that if the authorization request had one. - -**Example:** - -```json -{ - "access_token": "558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0", - "scope": "profile:email profile:avatar", - "token_type": "bearer", - "expires_in": 3600, - "refresh_token": "58d59cc97c3ca183b3a87a65eec6f93d5be051415b53afbf8491cc4c45dbb0c6", - "auth_at": 1422336613 -} -``` - -### POST /v1/destroy - -After a client is done using a token, the responsible thing to do is to -destroy the token afterwards. A client can use this route to do so. - -#### Request Parameters - -- `token` - The hex string token. - -**Example:** - -```sh -curl -v \ --X POST \ --H "Content-Type: application/json" \ -"https://oauth.accounts.firefox.com/v1/destroy" \ --d '{ - "token": "558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" -}' -``` - -#### Response - -A valid request will return an empty response, with a 200 status code. - - -### POST /v1/verify - -Attached services can post tokens to this endpoint to learn about which -user and scopes are permitted for the token. - -#### Request Parameters - -- `token`: A token string received from a client - -**Example:** - -```sh -curl -v \ --X POST \ --H "Content-Type: application/json" \ -"https://oauth.accounts.firefox.com/v1/verify" \ --d '{ - "token": "558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" -}' -``` - -#### Response - -A valid request will return JSON with these properties: - -- `user`: The uid of the respective user. -- `client_id`: The client_id of the respective client. -- `scope`: An array of scopes allowed for this token. -- `email`: **DEPRECATED** The email of the respective user. - -**Example:** - -```json -{ - "user": "5901bd09376fadaa076afacef5251b6a", - "client_id": "45defeda038a1c92", - "scope": ["profile:email", "profile:avatar"], - "email": "foo@example.com" -} -``` - -### GET /v1/jwks - -This endpoint returns the [JWKs](https://tools.ietf.org/html/rfc7517) -that are used for signing OpenID Connect id tokens. - -#### Request - -```sh -curl -v "https://oauth.accounts.firefox.com/v1/jwks" -``` - -#### Response - -A valid response will return JSON of the `keys`. - -**Example:** - -```json -{ - "keys": [ - "alg": "RS256", - "use": "sig", - "kty": "RSA", - "kid": "2015.12.02-1", - "n":"xaQHsKpu1KSK-YEMoLzZS7Xxciy3esGrhrrqW_JBrq3IRmeGLaqlE80zcpIVnStyp9tbet2niYTemt8ug591YWO5Y-S0EgQyFTxnGjzNOvAL6Cd2iGie9QeSehfFLNyRPdQiadYw07fw-h5gweMpVJs8nTgS-Bcorlw9JQM6Il1cUpbP0Lt-F_5qrzlaOiTEAAb4JGOusVh0n-MZfKt7w0mikauMH5KfhflwQDn4YTzRkWJzlldXr1Cs0ZkYzOwS4Hcoku7vd6lqCUO0GgZvkuvCFqdVKzpa4CGboNdfIjcGVF4f1CTQaQ0ao51cwLzq1pgi5aWYhVH7lJcm6O_BQw", - "e":"AQAC" - ] -} -``` - - -### POST /v1/post-keydata - -This endpoint returns the required scoped key metadata. - -#### Request - -```sh -curl -X POST \ - https://oauth.accounts.firefox.com/v1/key-data \ - -H 'cache-control: no-cache' \ - -H 'content-type: application/json' \ - -d '{ - "client_id": "aaa6b9b3a65a1871", - "assertion": "eyJhbGciOiJSUzI1NiJ9.eyJwdWJsaWMta2V5Ijp7Imt0eSI6IlJTQSIsIm4iOiJvWmdsNkpwM0Iwcm5BVXppNThrdS1iT0RvR3ZuUGNnWU1UdXQ1WkpyQkJiazBCdWU4VUlRQ0dnYVdrYU5Xb29INkktMUZ6SXU0VFpZYnNqWGJ1c2JRRlQxOGREUkN6VVRubFlXdVZXUzhoSWhKc3lhZHJwSHJOVkI1VndmSlRKZVgwTjFpczBXcU1qdUdOc2VMLXluYnFjOVhueElncFJaai05QnZqY2ZKYXNOUTNZdHR3VHZVaFJOLVFGNWgxQkY1MnA2QmdOTVBvWmQ5MC1EU0xydlpseXp6MEh0Q2tFZnNsc013czVkR0ExTlZ1dEwtcGVDeU50VTFzOEtFaDlzcGxXeF9lQlFybTlYQU1kYXp5ZWR6VUpJU1UyMjZmQzhEUHh5c0ZreXpCbjlDQnFDQUpTNjQzTGFydUVDaS1rMGhKOWFmM2JXTmJnWmpSNVJ2NXF4THciLCJlIjoiQVFBQiJ9LCJwcmluY2lwYWwiOnsiZW1haWwiOiIwNjIxMzM0YzIwNjRjNmYzNmJlOGFkOWE0N2M1NTliY2FwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9LCJpYXQiOjE1MDY5Njk2OTU0MzksImV4cCI6MTUwNjk2OTY5NjQzOSwiZnhhLXZlcmlmaWVkRW1haWwiOiIzMjM2NzJiZUBtb3ppbGxhLmNvbSIsImlzcyI6ImFwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9.hFZd5zFheXOFrXKkJvw6Vpv2l7ctlxuBTvuh5f_jLPAjZoJ9ri-vaJjL_WYBFUvS2xHzfx3-ldxLddyTKwCDAJeB_NkOFL_WJSrMet9C7_Z1hH9HmydeXIT82xJmhrwzW-WOO4ibQvRbocEFiNujynKsg1gS8v0iiYjIX-0cXCrlkxkbVx_8EXJFKDDOGzK9v7Zq6D7gkhP-CHEaNYaTHMn65tLQtBS6snGdaXlxoGHMWmDL6STbnJzWa7sa4QwHf-AgT1rUkQQAUHNa_XLZ0FEzqiCPctMadlihiUZL2V6vxIDBS4mHUF4qj0FvIMJflivDnJVkRNijDuP-h-Lh_A~eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJvYXV0aC5meGEiLCJleHAiOjE1MDY5Njk2OTY0MzksImlzcyI6ImFwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9.M5xyk3RffucgaavjbUm7Eqnt47hzeGbGa2VR3jnVEIlRHfz5S25Qf3ngejwee7XECvIywbaKWeijXFOwS-EkB-7qP1gl4oNJjPmbnCk7S1lgckLWvdMIU-HLGKjrN6Mw76__LzvAbsusSeGmsvTCIVuOJ49Xs3tC1fLyB_re0QNpCcS6AUnJ1KOxIMEM3Om7ysNO5F_AqcD3PwlEti5lbwSk8iP5TWL12C2Nkb_6Hxze_mA1NZNAHOips9bF2J7oy1hqGoMYj1XYZrsyjpPWEuZQATAPlKSjbh1hq-UtDeT7DlwEmIbIUd3JA8qh1MkHKGgavd4fIMap0IPmr9rs4A", - "scope": "https://identity.mozilla.com/apps/sample-scope-can-scope-key" -}' -``` - -#### Response - -A valid response will return JSON the scoped key information for every scope that has scoped keys: - -**Example:** - -```json -{ - "https://identity.mozilla.com/apps/sample-scope-can-scope-key": { - "identifier": "https://identity.mozilla.com/apps/sample-scope-can-scope-key", - "keyRotationSecret": "0000000000000000000000000000000000000000000000000000000000000000", - "keyRotationTimestamp": 1506970363512 - } -} -``` - -### GET /v1/client-tokens - -This endpoint returns a list of all clients with active OAuth tokens for the user, -including the the scopes granted to each client -and the last time each client was active. -It must be authenticated with an OAuth token bearing scope "clients:write". - -#### Request - -**Example:** - -```sh -curl -X GET \ - https://oauth.accounts.firefox.com/v1/client-tokens \ - -H 'cache-control: no-cache' \ - -H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" -``` - -#### Response - -A valid 200 response will be a JSON array -where each item as the following properties: - -- `id`: The hex id of the client. -- `name`: The string name of the client. -- `lastAccessTime`: Integer last-access time for the client. -- `lastAccessTimeFormatted`: Localized string last-access time for the client. -- `scope`: Sorted list of all scopes granted to the client. - -**Example:** - -```json -[ - { - "id": "5901bd09376fadaa", - "name": "Example", - "lastAccessTime": 1528334748000, - "lastAccessTimeFormatted": "13 days ago", - "scope": ["openid", "profile"] - }, - { - "id": "23d10a14f474ca41", - "name": "Example Two", - "lastAccessTime": 1476677854037, - "lastAccessTimeFormatted": "2 years ago", - "scope": ["profile:email", "profile:uid"] - } -] -``` - -### DELETE /v1/client-tokens/:id - -This endpoint deletes all tokens granted to a given client. -It must be authenticated with an OAuth token bearing scope "clients:write". - -#### Request Parameters - -- `id`: The `client_id` of the client whose tokens should be deleted. - -**Example:** - -```sh -curl -X DELETE - https://oauth.accounts.firefox.com/v1/client-tokens/5901bd09376fadaa - -H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" -``` - -#### Response - -A valid 200 response will return an empty JSON object. - - - -[client]: #get-v1clientid -[register]: #post-v1clientregister -[clients]: #get-v1clients -[client-update]: #post-v1clientid -[client-delete]: #delete-v1clientid -[redirect]: #get-v1authorization -[authorization]: #post-v1authorization -[token]: #post-v1token -[delete]: #post-v1destroy -[verify]: #post-v1verify -[developer-activate]: #post-v1developeractivate -[jwks]: #get-v1jwks -[key-data]: #post-v1post-keydata -[client-tokens]: #get-v1client-tokens -[client-tokens-delete]: #delete-v1client-tokensid - -[Service Clients]: ./service-clients.md diff --git a/fxa-oauth-server/docs/clients.md b/fxa-oauth-server/docs/clients.md deleted file mode 100644 index 76809b5..0000000 --- a/fxa-oauth-server/docs/clients.md +++ /dev/null @@ -1,68 +0,0 @@ -# Configuring OAuth clients - -## How to register a client manually - -Usually, when you connect applications to their OAuth resource server, they generate a client `id` and `secret` for you. In our case, we are the resource server. - -The `id` and `secret` keys, in this context, can be seen as a username and password. They do not need to be generated in relation between one and another. In other words, they are not public and private keys. - -With this procedure you will generate both client id and secret tokens to provide to your other applications and also partners who wants to leverage your identity provider service. Once you have the client id and secret, paste them in both the fxa-oauth-server AND your client service you want to bind using OAuth. - - -## Difference between same site and external consumers - -While other applications within your infrastructure would ideally be pre-approved at the user point of view, external consumers shouldn't be. This is why when we develop a service leveraging another site, the user gets a confirmation window. - -If you want to pre-approve your own web applications and prevent users in your accounts userbase to have a confirmation window, set the `trusted` flag to `true`. - - -## Installing a new consumer - -### Creating the client id and secret keys - -Use the [fxa-oauth-client][] CLI tool for registering new clients with your server. - -FxA OAuth development environments support `127.0.0.1` and `localhost` as valid `redirectUri` values to ease development. - -[fxa-oauth-client]: https://github.com/mozilla/fxa-oauth-client - -### OAuth resource server (a.k.a. `fxa-oauth-server`) - -Let's assume that the client in this example is the [123done](https://github.com/mozilla/123done) web application that you deployed at `https://clientapp.example.com` and that the OAuth route is available at `api/oauth` (this is specific to each client application, beware (!)) - -Add a new object literal within the `clients` array, that would look like: - -```json -{ - "clients": [ - { - "id": "<8-byte client id in hex>", - "hashedSecret": "<32-byte sha256 of the client secret in hex>", - "name": "123done", - "imageUri": "https://clientapp.example.com/static/img/logo100.png", - "redirectUri": "https://clientapp.example.com/api/oauth", - "trusted": true - } - ] -} -``` - -**NOTE:** the `trusted`, this would be for an internal application that you manage. - - -### OAuth clients - -This can be very different depending on your installed and supported version, you should have a look at the profile server and client application what are the required callbacks. - -```json -{ - "client_id": "<8-byte client id in hex>", - "client_secret": "<32-byte client secret in hex>", - "name": "123done", - "redirect_uri": "https://clientapp.example.com/api/oauth", - "signin_uri": "https://accounts.firefox.com/oauth/signin", - "oauth_uri": "https://oauth.accounts.firefox.com/v1", - "profile_uri": "https://profile.firefox.com/v1", - "scopes": "profile" -} -``` diff --git a/fxa-oauth-server/docs/pkce.md b/fxa-oauth-server/docs/pkce.md deleted file mode 100644 index e742f36..0000000 --- a/fxa-oauth-server/docs/pkce.md +++ /dev/null @@ -1,13 +0,0 @@ -# Firefox Accounts OAuth - PKCE Support - -> Proof Key for Code Exchange by OAuth Public Clients - -Firefox Accounts OAuth flow supports the [PKCE RFC7636](https://tools.ietf.org/html/rfc7636). -This feature helps us authenticate clients such as WebExtensions and Native apps. -Clients that do not have a server component or a secure way to store a `client_secret`. - -To better understand this protocol please read the [Proof Key for Code Exchange (RFC 7636) by Authlete Inc.](https://www.authlete.com/documents/article/pkce/index). - -Please see the [API](API.md) documentation that explains the support parameters - `code_challenge_method`, `code_challenge` and `code_verifier`. - -At this time Firefox Accounts requires you to use the `S256` flow, we do not support the `plain` code challenge method. diff --git a/fxa-oauth-server/docs/scopes.md b/fxa-oauth-server/docs/scopes.md deleted file mode 100644 index 79b2b15..0000000 --- a/fxa-oauth-server/docs/scopes.md +++ /dev/null @@ -1,199 +0,0 @@ -# OAuth Scopes - -Each authorization grant in OAuth has an associated "scope", -a list containing one or more "scope values" -that indicate what capabilities the granted token will have. -Each individual scope value indicates a particular capability, -such as the ability to read or write profile data, -or to access the user's data in a particular service. - -As defined in [RFC6749 Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3), -the scope of a token is expressed as -a list of space-delimited, case-sensitive strings, -and it is left up to the service -to define the format and semantics -of the individual scope values -that make up this string. - -This document defines the scope values -accepted in the Firefox Accounts ecosystem, -and the rules for parsing and validating them. - -## Short-name scope values - -FxA supports a small set of "short-name" scope values -that are identified by a short English word. -These correspond either to -scope values defined by external specifications -(e.g. OpenID Connect), -or to legacy scope values -introduced during early development. - -**No new short-name scope values should be added.** -Instead we prefer to use URLs for new scope values, -both to ensure uniqueness -and to simplify parsing rules. - -Short-name scope values imply read-only access by default, -with write access indicated by the suffix ":write". -The may also have "sub-scopes" -to indicate finer-grained access control. -Each name component may contain only ascii alphanumeric characters -and the underscore. - -For example: - -* `profile` indicates read-only access - to the user's profile data. -* `profile:write` indicates read/write access - to the user's profile data. -* `profile:display_name` indicates read-only access - to the user's display name, but not any other - profile data. -* `profile:email:write` indicates read/write access - to the user's email address. - -The following short-name scope values are recognized -in the FxA ecosystem. - -### Profile data - -* `profile`: access the user's profile data. -* `profile:uid`: access the user's opaque user id. -* `profile:email`: access the user's email address. -* `profile:locale`: access the user's locale. -* `profile:avatar`: access the user's avatar picture. -* `profile:display_name`: access the user's human-readable display name. -* `profile:amr`: access information about the user's authentication methods and 2FA status. - -### OpenID Connect - -* `openid`: used to request an OpenID Connect `id_token`. -* `email`: a synonym for `profile:email`, defined by the OIDC spec. - -### OAuth Client Management - -* `clients`: access the list of OAuth clients connected to a user's account. -* `oauth`: register a new OAuth client record. - -### Basket - -* `basket`: access the user's subscription data in - [basket](http://basket.readthedocs.io/) - - -## URL Scopes - -For new capabilities, scope values are represented as URLs. -This helps to ensure uniqueness -and reduces ambiguity in parsing. -URL-format scope value imply read/write access by default, -are compared as heirarchical resource references, -and use the hash fragment for permission qualifiers. -For example: - -* `https://identity.mozilla.com/apps/oldsync` indicates full - access to the user's data in Firefox Sync. -* `https://identity.mozilla.com/apps/oldsync/bookmarks` indicates - full access to the user's bookmark data in Firefox Sync, - but not to other data types. -* `https://identity.mozilla.com/apps/oldsync#read` indicates - read-only access to the user's data in Firefox Sync. -* `https://identity.mozilla.com/apps/oldsync/history#write` indicates - write-only access to the user's history data in Firefox Sync. - -To be a valid scope value, the URL must: - -* Be an absolute `https://` URL. -* Have no username, password, or query component. -* If present, have a fragment component consisting only of alphanumeric ascii characters and underscore. -* Remain unchanged when parsed and serialized following the rules in the - [WhatWG URL Spec](https://url.spec.whatwg.org). - -The following URL scope values are currently recognized by FxA: - -* `https://identity.mozilla.com/apps/oldsync`: access to data in Firefox Sync. -* `https://identity.mozilla.com/apps/notes`: access to data in Firefox Notes. - - -## Scope Matching and Implication - -We say that a scope value A *implies* another scope value B -if they are exactly equal, -or if A represents a more general capability than B. -Similarly, a scope A implies scope value B -if there is some scope value in A that implies B. -This is the basic operation used to check -permissions when processing an OAuth token. - -Consumers of OAuth tokens should avoid -directly parsing and comparing scopes where possible, -and instead use the existing implementation -in the `fxa-shared` node module. - -For consumers that must implement their own scope checking, -the rules for implication can be summarized as: - -* For URL scope values, A implies B if A is a parent resource of B. -* For short-name scope values, split on the ":" character, - and A implies B if either: - * B[-1] is not "write" and A is a prefix of B, or. - * A[-1] is "write", and: - * A[:-1] is a prefix of B, or - * B[-1] is "write" and A[:-1] is a prefix of B[:-1] - -More precisely, the algoritm for checking implication is: - -* If A is a `https://` URL, then: - * If B is not a `https://` URL, then fail. - * If the origin of B is different than that of A, then fail. - * If the path component list of A is not a prefix of the path - component list of B, then fail. - * If A has a fragment, then: - * If B does not have a fragment, then fail. - * If B has a fragment that differs from A, then fail. - * Otherwise, succeed. -* Otherwise: - * If B is a `https://` URL, then fail. - * Split A and B into components based on `:` delimiter. - * If the last component of B is `write`, then: - * If the last component of A is not `write`, then fail. - * If the last component of A is `write`, remove it. - * If A is not a prefix of B, then fail. - * Otherwise, succeed. - -Below are some testcases against which -scope-checking code can be validated. - -Valid implications: -* `profile:write` implies `profile`. -* `profile` implies `profile:email`. -* `profile:write` implies `profile:email`. -* `profile:write` implies `profile:email:write`. -* `profile:email:write` implies `profile:email`. -* `profile profile:email:write` implies `profile:email`. -* `profile profile:email:write` implies `profile:display_name`. -* `profile https://identity.mozilla.com/apps/oldsync` implies `profile`. -* `profile https://identity.mozilla.com/apps/oldsync` implies `https://identity.mozilla.com/apps/oldsync`. -* `https://identity.mozilla.com/apps/oldsync` implies `https://identity.mozilla.com/apps/oldsync#read`. -* `https://identity.mozilla.com/apps/oldsync` implies `https://identity.mozilla.com/apps/oldsync/bookmarks`. -* `https://identity.mozilla.com/apps/oldsync` implies `https://identity.mozilla.com/apps/oldsync/bookmarks#read`. -* `https://identity.mozilla.com/apps/oldsync#read` implies `https://identity.mozilla.com/apps/oldsync/bookmarks#read`. -* `https://identity.mozilla.com/apps/oldsync#read profile` implies `https://identity.mozilla.com/apps/oldsync/bookmarks#read`. - -Invalid implications: -* `profile:email:write` does *not* imply `profile`. -* `profile:email:write` does *not* imply `profile:write`. -* `profile:email` does *not* imply `profile:display_name`. -* `profilebogey` does *not* imply `profile`. -* `profile:write` does *not* imply `https://identity.mozilla.com/apps/oldsync`. -* `profile profile:email:write` does *not* imply `profile:write`. -* `https` does *not* imply `https://identity.mozilla.com/apps/oldsync`. -* `https://identity.mozilla.com/apps/oldsync` does *not* imply `profile`. -* `https://identity.mozilla.com/apps/oldsync#read` does *not* imply `https://identity.mozilla.com/apps/oldsync/bookmarks`. -* `https://identity.mozilla.com/apps/oldsync#write` does *not* imply `https://identity.mozilla.com/apps/oldsync/bookmarks#read`. -* `https://identity.mozilla.com/apps/oldsync/bookmarks` does *not* imply `https://identity.mozilla.com/apps/oldsync`. -* `https://identity.mozilla.com/apps/oldsync/bookmarks` does *not* imply `https://identity.mozilla.com/apps/oldsync/passwords`. -* `https://identity.mozilla.com/apps/oldsyncer` does *not* imply `https://identity.mozilla.com/apps/oldsync`. -* `https://identity.mozilla.com/apps/oldsync` does *not* imply `https://identity.mozilla.com/apps/oldsyncer`. -* `https://identity.mozilla.org/apps/oldsync` does *not* imply `https://identity.mozilla.com/apps/oldsync`. diff --git a/fxa-oauth-server/docs/service-clients.md b/fxa-oauth-server/docs/service-clients.md deleted file mode 100644 index 157d65b..0000000 --- a/fxa-oauth-server/docs/service-clients.md +++ /dev/null @@ -1,92 +0,0 @@ -# Service Clients - -Do you wish to allow users to authenticate to your app, as well as fetch -some information about them? Then you don't want to be a Service Client. -Be a regular client. - -Service Clients exist as privileged apps that need to be able to request -information about a user, without ever having received permission to do -so. Sounds nefarious. However, as said, these are **privileged** apps, -meaning they are also run by **us** (Mozilla). So these apps are not -gaining access to something that we don't already know. Plus, before -ever being promoted to a Service Client, real humans are involved to -make sure it's the correct use case. - -## All Powerful - -A Service Client exists by being in the config array `serviceClients`. -Each entry (if any) is an object, with the following values: - -- `id` - `String`: a unique hex value in identical format to regular - client ids. -- `name` - `String`: a plaintext name for the client that's friendly to - human readers. -- `scope` - `String`: space-separated scopes that this client will be - using when requesting access tokens. -- `jku` - `String`: a unique URL that will host a JWK Set. Unique as in - unique in the current list of Service Clients. - -## JKUs and JWK Sets - -Imagine a service client with the `jku` of `https://example.dom.ain/keys`. -The document hosted at that URL should contain something like: - -```json -{ - "keys":[{ - "kid":"key-id-can-whatever-1", - "use": "sig", - "kty":"RSA", - "n":"W_lCUvksZMVxW2JLNtoyPPshvSHng28H5FggSBGBjmzv3eHkMgRdc8hpOkgcPwXYxHdVM6udtVdXZtbGN8nUyQX8gxD3AJg-GSrH3UOsoArPLCmcxwIEpk4B0wqwP68oK8dQHt0iK3N-XeCnMpv75ULlVn3LEOZT8CsuNraVOthYeClUb8r1PjRwqRB06QGNqnnhcPMmh-6cRzQ9HmTMz6CDcugiH5n2sjrvpeBugEsnXt3KpzVdSc4usXrIEmLRuFjwFbkzoo7FiAtSoXxBqc074qz8ejm-V0-2Wv3p6ePeLODeYkPQho4Lb1TBdoidr9RHY29Out4mhzb4nUrHHQ", - "e":"AQAB" - }] -} -``` - -This JKU is important when making requests for access tokens. - -## JWTs and Authorization - -To request an access token, a service client must generate a signed [JWT][], -and then send it to our [/v1/token][] endpoint. - -Here's an example of creating a JWT in JavaScript: - -```js -var now = Math.floor(Date.now() / 1000); // in seconds -var header = { - alg: 'RS256', - typ: 'JWT', - jku: 'https://basket.mozilla.org/.well-known/jku', - kid: 'k1' -}; -var claims = { - scope: 'profile:email', - aud: 'https://oauth.accounts.firefox.com/v1/token', - iat: now, - exp: now + (60 * 5), - sub: '9b052aebbc48c8376257c777e2a7f009' -}; - -var token = base64(JSON.stringify(header)) + '.' + base64(JSON.stringify(claims)); -var sig = rsa256(Buffer.from(token, 'base64'), privateKey); -var jwt = token + '.' + base64(sig); -``` - -Once you have a signed JWT, you would make the following request: - -```sh -curl -v \ --X POST \ --H "Content-Type: application/json" \ -"https://oauth.accounts.firefox.com/v1/token" \ --d '{ - "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", - "assertion": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImprdSI6Imh0dHBzOi8vYmFza2V0LmFjY291bnRzLmZpcmVmb3guY29tLy53ZWxsLWtub3duL2prdSIsImtpZCI6ImsxIn0.eyJzdWIiOiI1OTAxYmQwOTM3NmZhZGFhNTkwMWJkMDkzNzZmYWRhYUBhY2NvdW50cy5maXJlZm94LmNvbSIsInNjb3BlIjoicHJvZmlsZTplbWFpbCIsImF1ZCI6Imh0dHBzOi8vb2F1dGguYWNjb3VudHMuZmlyZWZveC5jb20vdjEvdG9rZW4iLCJpYXQiOjE0NDM2NjI0ODEsImV4cCI6MTQ0MzY2Mjc4MX0.Kmwfq7yZrKpwrcZ78NTLPs8v4ijMhoKVNZ45VJY-skyK_XD_U5DJeKq8IE6PspU6B6p0DPkW1EEKeKOAbpyzFIBi9uG7l329x32JkzXGwybxannbGrdd5DFZbIaBSZDf-64MXbxGBGQ8xy18dfXmgbmNsvYPRZqqS2gmoM1EvWg" -}' -``` - -The response is described in the [API docs][/v1/token]. - -[/v1/token]: ./api.md#post-v1token -[JWT]: http://jwt.io/ diff --git a/fxa-oauth-server/grunttasks/bump.js b/fxa-oauth-server/grunttasks/bump.js deleted file mode 100644 index b3f9c62..0000000 --- a/fxa-oauth-server/grunttasks/bump.js +++ /dev/null @@ -1,25 +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/. */ - -// takes care of bumping the version number in package.json - -module.exports = function (grunt) { - 'use strict'; - - grunt.config('bump', { - options: { - files: ['package.json', 'npm-shrinkwrap.json'], - bumpVersion: true, - commit: true, - commitMessage: 'Release v%VERSION%', - commitFiles: ['package.json', 'npm-shrinkwrap.json', 'CHANGELOG.md'], - createTag: true, - tagName: 'v%VERSION%', - tagMessage: 'Version %VERSION%', - push: false, - pushTo: 'origin', - gitDescribeOptions: '--tags --always --abrev=1 --dirty=-d' - } - }); -}; diff --git a/fxa-oauth-server/grunttasks/changelog.js b/fxa-oauth-server/grunttasks/changelog.js deleted file mode 100644 index c8be3f8..0000000 --- a/fxa-oauth-server/grunttasks/changelog.js +++ /dev/null @@ -1,18 +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/. */ - -var fxaChangelog = require('fxa-conventional-changelog')(); - -module.exports = function (grunt) { - grunt.config('conventionalChangelog', { - options: { - changelogOpts: {}, - parserOpts: fxaChangelog.parserOpts, - writerOpts: fxaChangelog.writerOpts - }, - release: { - src: 'CHANGELOG.md' - } - }); -}; diff --git a/fxa-oauth-server/grunttasks/copyright.js b/fxa-oauth-server/grunttasks/copyright.js deleted file mode 100644 index 51b1539..0000000 --- a/fxa-oauth-server/grunttasks/copyright.js +++ /dev/null @@ -1,18 +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/. */ - -module.exports = function (grunt) { - 'use strict'; - - grunt.config('copyright', { - app: { - options: { - pattern: /This Source Code Form is subject to the terms of the Mozilla/ - }, - src: [ - '<%= mainJsFiles %>' - ] - } - }); -}; diff --git a/fxa-oauth-server/grunttasks/eslint.js b/fxa-oauth-server/grunttasks/eslint.js deleted file mode 100644 index bb26669..0000000 --- a/fxa-oauth-server/grunttasks/eslint.js +++ /dev/null @@ -1,16 +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/. */ - -module.exports = function (grunt) { - 'use strict'; - - grunt.config('eslint', { - options: { - eslintrc: '.eslintrc' - }, - app: [ - '<%= mainJsFiles %>' - ] - }); -}; diff --git a/fxa-oauth-server/grunttasks/lint.js b/fxa-oauth-server/grunttasks/lint.js deleted file mode 100644 index a231674..0000000 --- a/fxa-oauth-server/grunttasks/lint.js +++ /dev/null @@ -1,13 +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/. */ - -// meta grunt task to run other linters. - -module.exports = function (grunt) { - 'use strict'; - - grunt.registerTask('lint', [ - 'eslint' - ]); -}; diff --git a/fxa-oauth-server/grunttasks/release.js b/fxa-oauth-server/grunttasks/release.js deleted file mode 100644 index 668b335..0000000 --- a/fxa-oauth-server/grunttasks/release.js +++ /dev/null @@ -1,32 +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/. */ - -// -// A task to stamp a new version. -// -// -// * version is updated in package.json -// * changelog is generated. -// * git tag with version name is created. -// * git commit with updated package.json created. -// -// NOTE: This task will not push this commit for you. -// - - -module.exports = function (grunt) { - 'use strict'; - - grunt.registerTask('version', [ - 'bump-only:minor', - 'conventionalChangelog:release', - 'bump-commit' - ]); - - grunt.registerTask('version:patch', [ - 'bump-only:patch', - 'conventionalChangelog:release', - 'bump-commit' - ]); -}; diff --git a/fxa-oauth-server/grunttasks/server.js b/fxa-oauth-server/grunttasks/server.js deleted file mode 100644 index c156413..0000000 --- a/fxa-oauth-server/grunttasks/server.js +++ /dev/null @@ -1,17 +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/. */ - -module.exports = function (grunt) { - 'use strict'; - - grunt.config('nodemon', { - dev: { - script: 'bin/server.js', - options: { - args: ['--node-env=dev'] - } - } - }); - grunt.registerTask('server', ['nodemon:dev']); -}; diff --git a/fxa-oauth-server/lib/auth.js b/fxa-oauth-server/lib/auth.js deleted file mode 100644 index c689956..0000000 --- a/fxa-oauth-server/lib/auth.js +++ /dev/null @@ -1,60 +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/. */ - -const ScopeSet = require('fxa-shared').oauth.scopes; - -const AppError = require('./error'); -const logger = require('./logging')('server.auth'); -const token = require('./token'); -const validators = require('./validators'); - -const WHITELIST = require('./config').get('admin.whitelist').map(function(re) { - logger.verbose('compiling.whitelist', re); - return new RegExp(re); -}); - -exports.AUTH_STRATEGY = 'dogfood'; -exports.AUTH_SCHEME = 'bearer'; - -exports.SCOPE_CLIENT_MANAGEMENT = ScopeSet.fromArray(['oauth']); - -exports.strategy = function() { - return { - authenticate: async function dogfoodStrategy(req, h) { - var auth = req.headers.authorization; - logger.debug('check.auth', { header: auth }); - if (! auth || auth.indexOf('Bearer ') !== 0) { - throw AppError.unauthorized('Bearer token not provided'); - } - var tok = auth.split(' ')[1]; - - if (! validators.HEX_STRING.test(tok)) { - throw AppError.unauthorized('Illegal Bearer token'); - } - - return token.verify(tok).then(function tokenFound(details) { - if (details.scope.contains(exports.SCOPE_CLIENT_MANAGEMENT)) { - logger.debug('check.whitelist'); - var blocked = ! WHITELIST.some(function(re) { - return re.test(details.email); - }); - if (blocked) { - logger.warn('whitelist.blocked', { - email: details.email, - token: tok - }); - throw AppError.forbidden(); - } - } - - logger.info('success', details); - details.scope = details.scope.getScopeValues(); - return h.authenticated({credentials: details}); - }, function noToken(err) { - logger.debug('error', err); - throw AppError.unauthorized('Bearer token invalid'); - }); - } - }; -}; diff --git a/fxa-oauth-server/lib/auth_bearer.js b/fxa-oauth-server/lib/auth_bearer.js deleted file mode 100644 index 50516f2..0000000 --- a/fxa-oauth-server/lib/auth_bearer.js +++ /dev/null @@ -1,47 +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/. */ - -const ScopeSet = require('fxa-shared').oauth.scopes; - -const AppError = require('./error'); -const logger = require('./logging')('server.auth_bearer'); -const token = require('./token'); -const validators = require('./validators'); - -const authName = 'authBearer'; -const authOAuthScope = ScopeSet.fromArray(['clients:write']); - -exports.AUTH_STRATEGY = authName; -exports.AUTH_SCHEME = authName; - -exports.SCOPE_CLIENT_WRITE = authOAuthScope; - -exports.strategy = function() { - return { - authenticate: async function authBearerStrategy(req, h) { - var auth = req.headers.authorization; - - logger.debug(authName + '.check', { header: auth }); - if (! auth || auth.indexOf('Bearer ') !== 0) { - throw AppError.unauthorized('Bearer token not provided'); - } - var tok = auth.split(' ')[1]; - - if (! validators.HEX_STRING.test(tok)) { - throw AppError.unauthorized('Illegal Bearer token'); - } - - return token.verify(tok).then(function tokenFound(details) { - logger.info(authName + '.success', details); - details.scope = details.scope.getScopeValues(); - return h.authenticated({ - credentials: details - }); - }, function noToken(err) { - logger.debug(authName + '.error', err); - throw AppError.unauthorized('Bearer token invalid'); - }); - } - }; -}; diff --git a/fxa-oauth-server/lib/browserid.js b/fxa-oauth-server/lib/browserid.js deleted file mode 100644 index f1dad32..0000000 --- a/fxa-oauth-server/lib/browserid.js +++ /dev/null @@ -1,105 +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/. */ - - -const Joi = require('joi'); - -const AppError = require('./error'); -const config = require('./config'); -const logger = require('./logging')('assertion'); -const P = require('./promise'); - -const HEX_STRING = /^[0-9a-f]+$/; -const CLAIMS_SCHEMA = Joi.object({ - 'uid': Joi.string().length(32).regex(HEX_STRING).required(), - 'fxa-generation': Joi.number().integer().min(0).required(), - 'fxa-verifiedEmail': Joi.string().max(255).required(), - 'fxa-lastAuthAt': Joi.number().integer().min(0).required(), - 'fxa-tokenVerified': Joi.boolean().optional(), - 'fxa-amr': Joi.array().items(Joi.string().alphanum()).optional(), - 'fxa-aal': Joi.number().integer().min(0).max(3).optional(), - 'fxa-profileChangedAt': Joi.number().integer().min(0).optional() -}).options({ stripUnknown: true }); - -const AUDIENCE = config.get('publicUrl'); - -const request = require('request').defaults({ - url: config.get('browserid.verificationUrl'), - pool: { - maxSockets: config.get('browserid.maxSockets') - } -}); - -function unb64(text) { - return Buffer.from(text, 'base64').toString('utf8'); -} - -function Assertion(assertion) { - this.assertion = assertion; -} - -Assertion.prototype.toJSON = function() { - const parts = this.assertion.split('.'); - const ass = JSON.parse(unb64(parts[1])); - ass.pubkey = ass.publicKey = ass['public-key'] = undefined; - try { - return { - header: JSON.parse(unb64(parts[0])), - assertion: ass, - cert: JSON.parse(unb64(parts[3])) - }; - } catch (ex) { - return ex; - } -}; - -module.exports = function verifyAssertion(assertion) { - logger.verbose('assertion', new Assertion(assertion)); - const d = P.defer(); - const opts = { - json: { - assertion: assertion, - audience: AUDIENCE - } - }; - request.post(opts, (err, res, body) => { - if (err) { - logger.error('verify.error', err); - return d.reject(err); - } - - function error(msg, val) { - logger.info('invalidAssertion', { - msg: msg, - val: val, - assertion: assertion - }); - d.reject(AppError.invalidAssertion()); - } - - if (! body || body.status !== 'okay') { - return error('non-okay response', body); - } - const email = body.email; - const parts = email.split('@'); - const allowedIssuer = config.get('browserid.issuer'); - if (parts.length !== 2 || parts[1] !== allowedIssuer) { - return error('invalid email', email); - } - if (body.issuer !== allowedIssuer) { - return error('invalid issuer', body.issuer); - } - const uid = parts[0]; - - const claims = body.idpClaims || {}; - claims.uid = uid; - CLAIMS_SCHEMA.validate(claims, (err, claims) => { - if (err) { - return error(err, claims); - } - return d.resolve(claims); - }); - }); - return d.promise; -}; diff --git a/fxa-oauth-server/lib/config.js b/fxa-oauth-server/lib/config.js deleted file mode 100644 index bb90ffe..0000000 --- a/fxa-oauth-server/lib/config.js +++ /dev/null @@ -1,406 +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/. */ - -const assert = require('assert'); -const fs = require('fs'); -const path = require('path'); - -const convict = require('convict'); -var DEFAULT_SUPPORTED_LANGUAGES = require('fxa-shared').l10n.supportedLanguages; - -const conf = convict({ - admin: { - whitelist: { - doc: 'An array of regexes. Passing any one will get through.', - default: ['@mozilla\\.com$'] - } - }, - api: { - version: { - doc: 'Number part of versioned endpoints - ex: /v1/token', - default: 1 - } - }, - allowHttpRedirects: { - arg: 'allowHttpRedirects', - doc: 'If true, then it allows http OAuth redirect uris', - env: 'ALLOW_HTTP_REDIRECTS', - format: Boolean, - default: false - }, - browserid: { - issuer: { - doc: 'We only accept assertions from this issuer', - env: 'ISSUER', - default: 'api.accounts.firefox.com' - }, - maxSockets: { - doc: 'The maximum number of connections that the pool can use at once.', - env: 'BROWSERID_MAX_SOCKETS', - default: 10 - }, - verificationUrl: { - doc: 'URL to the remote verifier we will use for fxa-assertions', - format: 'url', - env: 'VERIFICATION_URL', - default: 'https://verifier.accounts.firefox.com/v2' - } - }, - clients: { - doc: 'Some pre-defined clients that will be inserted into the DB', - env: 'OAUTH_CLIENTS', - default: [] - }, - scopes: { - doc: 'Some pre-defined list of scopes that will be inserted into the DB', - env: 'OAUTH_SCOPES', - default: [] - }, - clientAddressDepth: { - doc: 'location of the client ip address in the remote address chain', - format: Number, - env: 'CLIENT_ADDRESS_DEPTH', - default: 3 - }, - contentUrl: { - doc: 'URL to UI page in fxa-content-server that starts OAuth flow', - format: 'url', - env: 'CONTENT_URL', - default: 'https://accounts.firefox.com/oauth/' - }, - db: { - driver: { - env: 'DB', - format: ['mysql', 'memory'], - default: 'memory' - }, - autoUpdateClients: { - doc: 'If true, update clients from config file settings', - env: 'DB_AUTO_UPDATE_CLIENTS', - format: Boolean, - default: true - } - }, - encrypt: { - hashAlg: { - doc: 'Hash algorithm for storing secrets/codes/tokens.', - default: 'sha256' - } - }, - env: { - arg: 'node-env', - doc: 'The current node.js environment', - env: 'NODE_ENV', - format: ['dev', 'test', 'stage', 'prod'], - default: 'prod' - }, - events: { - region: { - doc: 'AWS Region of fxa account events', - format: String, - default: '', - env: 'FXA_EVENTS_REGION' - }, - queueUrl: { - doc: 'SQS queue url for fxa account events', - format: String, - default: '', - env: 'FXA_EVENTS_QUEUE_URL' - } - }, - expiration: { - accessToken: { - doc: 'Access Tokens maximum expiration (can live shorter)', - format: 'duration', - default: '2 weeks', - env: 'FXA_EXPIRATION_ACCESS_TOKEN' - }, - accessTokenExpiryEpoch: { - doc: 'Timestamp after which access token expiry is actively enforced', - format: 'timestamp', - default: '2017-01-01', - env: 'FXA_EXPIRATION_ACCESS_TOKEN_EXPIRY_EPOCH' - }, - code: { - doc: 'Clients must trade codes for tokens before they expire', - format: 'duration', - default: '15 minutes', - env: 'FXA_EXPIRATION_CODE' - }, - keyDataAuth: { - doc: 'Key data can only be fetched if lastAuthAt is within this duration', - format: 'duration', - default: '1 hour', - env: 'FXA_EXPIRATION_KEY_DATA_AUTH' - } - }, - refreshToken: { - updateAfter: { - doc: 'lastUsedAt only gets updated after this delay', - format: 'duration', - default: '24 hours', - env: 'FXA_REFRESH_TOKEN_UPDATE_AFTER' - } - }, - git: { - commit: { - doc: 'Commit SHA when in stage/production', - format: String, - default: '' - } - }, - hpkpConfig: { - enabled: { - default: false, - doc: 'Feature flag for appending HPKP headers', - format: Boolean, - env: 'HPKP_ENABLE' - }, - reportOnly: { - default: true, - doc: 'Enable report only mode', - format: Boolean, - env: 'HPKP_REPORT_ONLY' - }, - reportUri: { - default: '', - doc: 'Enable report only mode', - format: String, - env: 'HPKP_REPORT_URI' - }, - includeSubDomains: { - default: true, - doc: 'Include Sub-Domains', - format: Boolean, - env: 'HPKP_INCLUDE_SUBDOMAINS' - }, - maxAge: { - default: 1, - doc: 'Max age for HPKP headers (seconds)', - format: Number, - env: 'HPKP_MAX_AGE' - }, - sha256s: { - default: [], - doc: 'Supported pin-sha256s', - format: Array, - env: 'HPKP_PIN_SHA256' - } - }, - localRedirects: { - doc: 'When true, `localhost` and `127.0.0.1` always are legal redirects.', - default: false, - env: 'FXA_OAUTH_LOCAL_REDIRECTS' - }, - logging: { - app: { - default: 'fxa-oauth-server', - env: 'LOG_APP' - }, - level: { - default: 'info', - env: 'LOG_LEVEL' - }, - fmt: { - format: ['heka', 'pretty'], - default: 'heka', - env: 'LOG_FORMAT' - } - }, - mysql: { - createSchema: { - default: true, - env: 'CREATE_MYSQL_SCHEMA' - }, - user: { - default: 'root', - env: 'MYSQL_USERNAME' - }, - password: { - default: '', - env: 'MYSQL_PASSWORD' - }, - database: { - default: 'fxa_oauth', - env: 'MYSQL_DATABASE' - }, - host: { - default: '127.0.0.1', - env: 'MYSQL_HOST' - }, - port: { - default: '3306', - env: 'MYSQL_PORT' - }, - connectionLimit: { - doc: 'The maximum number of connections that the pool can use at once.', - default: 10, - env: 'MYSQL_CONNECTION_LIMIT' - } - }, - openid: { - keyFile: { - doc: 'Path to Private key JWK to sign id_tokens', - format: String, - default: '', - env: 'FXA_OPENID_KEYFILE' - }, - oldKeyFile: { - doc: 'Path to previous key that was used to sign id_tokens', - format: String, - default: '', - env: 'FXA_OPENID_OLDKEYFILE' - }, - key: { - doc: 'Private JWK to sign id_tokens', - default: {}, - env: 'FXA_OPENID_KEY' - }, - oldKey: { - doc: 'The previous public key that was used to sign id_tokens', - default: {}, - env: 'FXA_OPENID_OLDKEY' - }, - issuer: { - // this should match `issuer` in the 'OpenID Provider Metadata' document - // from the fxa-content-server - doc: 'The value of the `iss` property of the id_token', - default: 'https://accounts.firefox.com', - env: 'FXA_OPENID_ISSUER' - }, - ttl: { - doc: 'Number of milliseconds until id_token should expire', - default: '5 minutes', - format: 'duration', - env: 'FXA_OPENID_TTL' - } - }, - publicUrl: { - format: 'url', - default: 'http://127.0.0.1:9010', - env: 'PUBLIC_URL' - }, - server: { - host: { - env: 'HOST', - default: '127.0.0.1' - }, - port: { - env: 'PORT', - format: 'port', - default: 9010 - } - }, - serverInternal: { - host: { - env: 'HOST_INTERNAL', - default: '127.0.0.1' - }, - port: { - env: 'PORT_INTERNAL', - format: 'port', - default: 9011 - } - }, - serviceClients: { - doc: 'Clients that can make oauth requests for any user', - default: [] - }, - i18n: { - defaultLanguage: { - format: String, - default: 'en', - env: 'DEFAULT_LANG' - }, - supportedLanguages: { - format: Array, - default: DEFAULT_SUPPORTED_LANGUAGES, - env: 'SUPPORTED_LANGS' - } - }, - unique: { - clientSecret: { - doc: 'Bytes of generated client_secrets', - default: 32 - }, - code: { - doc: 'Bytes of generated codes', - default: 32 - }, - id: { - doc: 'Bytes of generated DB ids', - default: 8 - }, - token: { - doc: 'Bytes of generated tokens', - default: 32 - }, - developerId: { - doc: 'Bytes of generated developer ids', - default: 16 - } - }, - cacheControl: { - doc: 'Hapi: a string with the value of the "Cache-Control" header when caching is disabled', - format: String, - default: 'private, no-cache, no-store, must-revalidate' - }, - sentryDsn: { - doc: 'Sentry DSN for error and log reporting', - default: '', - format: 'String', - env: 'SENTRY_DSN' - } -}); - -var envConfig = path.join(__dirname, '..', 'config', conf.get('env') + '.json'); -var files = (envConfig + ',' + process.env.CONFIG_FILES) - .split(',').filter(fs.existsSync); -conf.loadFile(files); - -var options = { - allowed: 'strict' -}; - -conf.validate(options); - -// custom validation, since we cant yet specify rules for inside arrays -conf.get('serviceClients').forEach(function(client) { - assert(client.id, 'client id required'); - assert.equal(client.id.length, 16, 'client id must be 16 hex digits'); - assert.equal(Buffer.from(client.id, 'hex').toString('hex'), client.id, - 'client id must be 16 hex digits'); - assert.equal(typeof client.name, 'string', 'client name required'); - assert.equal(typeof client.scope, 'string', 'client scope required'); - assert.equal(typeof client.jku, 'string', 'client jku required'); -}); - -// Replace openid key if file specified -if (conf.get('openid.keyFile')){ - conf.set('openid.key', require(conf.get('openid.keyFile'))); -} - -if (conf.get('openid.oldKeyFile')){ - conf.set('openid.oldKey', require(conf.get('openid.oldKeyFile'))); -} - -var key = conf.get('openid.key'); -assert.equal(key.kty, 'RSA', 'openid.key.kty must be RSA'); -assert(key.kid, 'openid.key.kid is required'); -assert(key.n, 'openid.key.n is required'); -assert(key.e, 'openid.key.e is required'); -assert(key.d, 'openid.key.d is required'); - -var oldKey = conf.get('openid.oldKey'); -if (Object.keys(oldKey).length) { - assert.equal(oldKey.kty, 'RSA', 'openid.oldKey.kty must be RSA'); - assert(oldKey.kid, 'openid.oldKey.kid is required'); - assert.notEqual(key.kid, oldKey.kid, - 'openid.key.kid must differ from oldKey'); - assert(oldKey.n, 'openid.oldKey.n is required'); - assert(oldKey.e, 'openid.oldKey.e is required'); - assert(! oldKey.d, 'openid.oldKey.d is forbidden'); -} - -module.exports = conf; diff --git a/fxa-oauth-server/lib/db/helpers.js b/fxa-oauth-server/lib/db/helpers.js deleted file mode 100644 index bc176ff..0000000 --- a/fxa-oauth-server/lib/db/helpers.js +++ /dev/null @@ -1,85 +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/. */ - -/*** - * This module consists of helpers that are used by both memory and MySQL database engines. - **/ - -const unbuf = require('buf').unbuf.hex; -const ScopeSet = require('fxa-shared').oauth.scopes; - -module.exports = { - /** - * This helper takes a list of active oauth tokens and produces an aggregate - * summary of the active clients, by: - * - * * merging all scopes into a single set - * * taking the max token creation time a the last access time - * - * The resulting array is in sorted order by last access time, then client name. - * - * @param {Array} activeClientTokens - * An array of OAuth tokens, annotated with client name: - * (OAuth client) name|(OAuth client) id|(Token) createdAt|(Token) scope - * @returns {Array} - */ - aggregateActiveClients: function aggregateActiveClients(activeClientTokens) { - if (! activeClientTokens) { - return []; - } - - // Create an Object that stores unique OAuth client data - var activeClients = {}; - activeClientTokens.forEach(function (clientTokenObj) { - var clientIdHex = unbuf(clientTokenObj.id); - - if (! activeClients[clientIdHex]) { - // add the OAuth client if not already in the Object - activeClients[clientIdHex] = { - id: clientTokenObj.id, - name: clientTokenObj.name, - lastAccessTime: clientTokenObj.createdAt, - scope: ScopeSet.fromArray([]) - }; - } - - activeClients[clientIdHex].scope.add(clientTokenObj.scope); - - var clientTokenTime = clientTokenObj.createdAt; - if (clientTokenTime > activeClients[clientIdHex].lastAccessTime) { - // only update the createdAt if it is newer - activeClients[clientIdHex].lastAccessTime = clientTokenTime; - } - }); - - // Sort the scopes alphabetically, convert the Object structure to an array - var activeClientsArray = Object.keys(activeClients).map(function (key) { - activeClients[key].scope = activeClients[key].scope.getScopeValues().sort(); - return activeClients[key]; - }); - - // Sort the final Array structure first by lastAccessTime and then name - activeClientsArray.sort(function(a, b) { - if (b.lastAccessTime > a.lastAccessTime) { - return 1; - } - - if (b.lastAccessTime < a.lastAccessTime) { - return -1; - } - - if (a.name > b.name) { - return 1; - } - - if (a.name < b.name) { - return -1; - } - - return 0; - }); - - return activeClientsArray; - } -}; diff --git a/fxa-oauth-server/lib/db/index.js b/fxa-oauth-server/lib/db/index.js deleted file mode 100644 index 63b4778..0000000 --- a/fxa-oauth-server/lib/db/index.js +++ /dev/null @@ -1,214 +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/. */ - -const unbuf = require('buf').unbuf.hex; - -const P = require('../promise'); - -const config = require('../config'); -const encrypt = require('../encrypt'); -const logger = require('../logging')('db'); -const klass = config.get('db.driver') === 'mysql' ? - require('./mysql') : require('./memory'); -const unique = require('../unique'); - -function clientEquals(configClient, dbClient) { - var props = Object.keys(configClient); - for (var i = 0; i < props.length; i++) { - var prop = props[i]; - var configProp = unbuf(configClient[prop]); - var dbProp = unbuf(dbClient[prop]); - if (configProp !== dbProp) { - logger.debug('clients.differ', { - prop: prop, - configProp: configProp, - dbProp: dbProp - }); - return false; - } - } - return true; -} - -function convertClientToConfigFormat(client) { - var out = {}; - - for (var key in client) { - if (key === 'hashedSecret' || key === 'hashedSecretPrevious') { - out[key] = unbuf(client[key]); - } else if (key === 'trusted' || key === 'canGrant') { - out[key] = !! client[key]; // db stores booleans as 0 or 1. - } else if (typeof client[key] !== 'function') { - out[key] = unbuf(client[key]); - } - } - return out; -} - -function preClients() { - var clients = config.get('clients'); - if (clients && clients.length) { - logger.debug('predefined.loading', { clients: clients }); - return P.all(clients.map(function(c) { - if (c.secret) { - console.error('Do not keep client secrets in the config file.' // eslint-disable-line no-console - + ' Use the `hashedSecret` field instead.\n\n' - + '\tclient=%s has `secret` field\n' - + '\tuse hashedSecret="%s" instead', - c.id, - unbuf(encrypt.hash(c.secret))); - return P.reject(new Error('Do not keep client secrets in the config file.')); - } - - // ensure the required keys are present. - var err = null; - var REQUIRED_CLIENTS_KEYS = [ 'id', 'hashedSecret', 'name', 'imageUri', - 'redirectUri', 'trusted', 'canGrant' ]; - REQUIRED_CLIENTS_KEYS.forEach(function(key) { - if (! (key in c)) { - var data = { key: key, name: c.name || 'unknown' }; - logger.error('client.missing.keys', data); - err = new Error('Client config has missing keys'); - } - }); - if (err) { - return P.reject(err); - } - - // ensure booleans are boolean and not undefined - c.trusted = !! c.trusted; - c.canGrant = !! c.canGrant; - c.publicClient = !! c.publicClient; - - // Modification of the database at startup in production and stage is - // not preferred. This option will be set to false on those stacks. - if (! config.get('db.autoUpdateClients')) { - return P.resolve(); - } - - return exports.getClient(c.id).then(function(client) { - if (client) { - client = convertClientToConfigFormat(client); - logger.info('client.compare', { id: c.id }); - if (clientEquals(client, c)) { - logger.info('client.compare.equal', { id: c.id }); - } else { - logger.warn('client.compare.differs', { - id: c.id, - before: client, - after: c - }); - return exports.updateClient(c); - } - } else { - return exports.registerClient(c); - } - }); - })); - } else { - return P.resolve(); - } -} - -function serviceClients() { - var clients = config.get('serviceClients'); - if (clients && clients.length) { - logger.debug('serviceClients.loading', clients); - - return P.all(clients.map(function(client) { - return exports.getClient(client.id).then(function(existing) { - if (existing) { - logger.verbose('seviceClients.existing', client); - return; - } - - return exports.registerClient({ - id: client.id, - name: client.name, - hashedSecret: encrypt.hash(unique.secret()), - imageUri: '', - redirectUri: '', - trusted: true, - canGrant: false - }); - }); - })); - } -} - -/** - * Insert pre-defined list of scopes into the DB - */ -function scopes() { - var scopes = config.get('scopes'); - if (scopes && scopes.length) { - logger.debug('scopes.loading', JSON.stringify(scopes)); - - return P.all(scopes.map(function(s) { - return exports.getScope(s.scope).then(function(existing) { - if (existing) { - logger.verbose('scopes.existing', s); - return; - } - - return exports.registerScope(s); - }); - })); - } -} - -var driver; -function withDriver() { - if (driver) { - return P.resolve(driver); - } - var p; - if (config.get('db.driver') === 'mysql') { - p = klass.connect(config.get('mysql')); - } else { - p = klass.connect(); - } - return p.then(function(store) { - logger.debug('connected', { driver: config.get('db.driver') }); - driver = store; - }).then(exports._initialClients).then(function() { - return driver; - }); -} - -const proxyReturn = logger.isEnabledFor(logger.VERBOSE) ? - function verboseReturn(promise, method) { - return promise.then(function(ret) { - logger.verbose('proxied', { method: method, ret: ret }); - return ret; - }); - } : function identity(x) { - return x; - }; - -function proxy(method) { - return function proxied() { - var args = arguments; - return withDriver().then(function(driver) { - logger.verbose('proxying', { method: method, args: args }); - return proxyReturn(driver[method].apply(driver, args), method); - }).catch(function(err) { - logger.error(method, err); - throw err; - }); - }; -} - - -Object.keys(klass.prototype).forEach(function(key) { - exports[key] = proxy(key); -}); - -exports.disconnect = function disconnect() { - driver = null; -}; - -exports._initialClients = function() { - return preClients().then(serviceClients).then(scopes); -}; diff --git a/fxa-oauth-server/lib/db/memory.js b/fxa-oauth-server/lib/db/memory.js deleted file mode 100644 index b175a93..0000000 --- a/fxa-oauth-server/lib/db/memory.js +++ /dev/null @@ -1,518 +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/. */ - -const buf = require('buf').hex; -const unbuf = require('buf').unbuf.hex; - -const config = require('../config'); -const encrypt = require('../encrypt'); -const helpers = require('./helpers'); -const logger = require('../logging')('db.memory'); -const P = require('../promise'); -const unique = require('../unique'); - -const MAX_TTL = config.get('expiration.accessToken'); - -/* - * MemoryStore structure: - * MemoryStore = { - * clients: { - * : { - * id: , - * hashedSecret: , - * hashedSecretPrevious: , - * name: , - * imageUri: , - * redirectUri: , - * trusted: , - * createdAt: - * } - * }, - * codes: { - * : { - * clientId: , - * userId: , - * code: - * scope: , - * authAt: , - * amr: [] - * aal: , - * createdAt: , - * offline: , - * codeChallengeMethod: , - * codeChallenge: , - * } - * }, - * developers: { - * : { - * developerId: , - * email: , - * createdAt: - * } - * }, - * clientDevelopers: { - * : { - * developerId: , - * clientId: , - * createdAt: - * } - * }, - * tokens: { - * : { - * token: , - * clientId: , - * userId: , - * type: , - * scope: , - * createdAt: , - * expiresAt: - * } - * }, - * refreshTokens: { - * : { - * token: , - * clientId: , - * userId: , - * scope: , - * createdAt: , - * lastUsedAt: - * } - * }, - * scopes: { - * : { - * scope: , - * hasScopedKeys: - * } - * } - */ -function MemoryStore() { - if (! (this instanceof MemoryStore)) { - return new MemoryStore(); - } - this.clients = {}; - this.codes = {}; - this.tokens = {}; - this.developers = {}; - this.clientDevelopers = {}; - this.refreshTokens = {}; - this.scopes = {}; -} - -MemoryStore.connect = function memoryConnect() { - return P.resolve(new MemoryStore()); -}; - -function clone(obj) { - if (! obj) { - return obj; - } - var clone = {}; - for (var k in obj) { - clone[k] = obj[k]; - } - return clone; -} - -function deleteByUserId(object, userId) { - var ids = Object.keys(object); - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - if (object[id].userId === userId) { - delete object[id]; - } - } -} - -function deleteByClientId(object, clientId) { - var ids = Object.keys(object); - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - if (unbuf(object[id].clientId) === clientId) { - delete object[id]; - } - } -} - -MemoryStore.prototype = { - ping: function ping() { - return P.resolve({}); - }, - registerClient: function registerClient(client) { - if (client.id) { - client.id = buf(client.id); - } else { - client.id = unique.id(); - } - var hex = unbuf(client.id); - logger.debug('registerClient', { name: client.name, id: hex }); - client.createdAt = new Date(); - client.imageUri = client.imageUri || ''; - client.redirectUri = client.redirectUri || ''; - client.canGrant = !! client.canGrant; - client.trusted = !! client.trusted; - this.clients[hex] = client; - client.hashedSecret = client.hashedSecret; - client.hashedSecretPrevious = client.hashedSecretPrevious || ''; - return P.resolve(client); - }, - updateClient: function updateClient(client) { - if (! client.id) { - return P.reject(new Error('Update client needs an id')); - } - var hex = unbuf(client.id); - if (! this.clients[hex]) { - return P.reject(new Error('Client does not exist')); - } - var old = this.clients[hex]; - for (var key in client) { - if (key === 'id') { - // nothing - } else if (key === 'hashedSecret') { - old.hashedSecret = buf(client[key]); - } else if (key === 'hashedSecretPrevious') { - old.hashedSecretPrevious = buf(client[key]); - } else if (client[key] !== undefined) { - old[key] = client[key]; - } - } - return P.resolve(old); - }, - getClient: function getClient(id) { - return P.resolve(this.clients[unbuf(id)]); - }, - getClients: function getClients(email) { - var self = this; - - return this.getDeveloper(email) - .then(function (developer) { - if (! developer) { - return []; - } - - var clients = []; - - Object.keys(self.clientDevelopers).forEach(function(key) { - var entry = self.clientDevelopers[key]; - if (entry.developerId === developer.developerId) { - clients.push(unbuf(entry.clientId)); - } - }); - - return clients.map(function(id) { - var client = self.clients[id]; - - return { - id: client.id, - name: client.name, - imageUri: client.imageUri, - redirectUri: client.redirectUri, - canGrant: client.canGrant, - trusted: client.trusted - }; - }, this); - }); - }, - removeClient: function removeClient(id) { - delete this.clients[unbuf(id)]; - return P.resolve(); - }, - generateCode: function generateCode(codeObj) { - codeObj = clone(codeObj); - codeObj.createdAt = new Date(); - var code = unique.code(); - codeObj.code = encrypt.hash(code); - this.codes[unbuf(codeObj.code)] = codeObj; - return P.resolve(code); - }, - getCode: function getCode(code) { - return P.resolve(clone(this.codes[unbuf(encrypt.hash(code))])); - }, - removeCode: function removeCode(code) { - delete this.codes[unbuf(encrypt.hash(code))]; - return P.resolve(); - }, - generateAccessToken: function generateAccessToken(vals) { - var token = unique.token(); - var now = new Date(); - var t = { - clientId: vals.clientId, - userId: vals.userId, - email: vals.email, - scope: vals.scope, - type: 'bearer', - createdAt: now, - // ttl is in seconds - expiresAt: new Date(+now + (vals.ttl * 1000 || MAX_TTL)), - token: encrypt.hash(token), - profileChangedAt: vals.profileChangedAt || 0 - }; - var ret = clone(t); - this.tokens[unbuf(t.token)] = t; - ret.token = token; - return P.resolve(ret); - }, - getAccessToken: function getAccessToken(token) { - return P.resolve(clone(this.tokens[unbuf(token)])); - }, - removeAccessToken: function removeAccessToken(id) { - delete this.tokens[unbuf(id)]; - return P.resolve(); - }, - - /** - * Get all services that have have non-expired tokens - * @param {String} uid User ID as hex - * @returns {Promise} - */ - getActiveClientsByUid: function getActiveClientsByUid(uid) { - var self = this; - if (! uid) { - return P.reject(new Error('Uid is required')); - } - - var activeClientTokens = []; - var ids = Object.keys(this.tokens); - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - if (this.tokens[id].userId.toString('hex') === uid) { - var clientIdHex = unbuf(this.tokens[id].clientId); - var client = self.clients[clientIdHex]; - if (client.canGrant === false) { - activeClientTokens.push({ - id: this.tokens[id].clientId, - createdAt: this.tokens[id].createdAt, - name: client.name, - scope: this.tokens[id].scope - }); - } - } - } - - return P.resolve(helpers.aggregateActiveClients(activeClientTokens)); - }, - - /** - * Delete all authorization grants for some clientId and uid. - * - * @param {String} clientId Client ID - * @param {String} uid User Id as Hex - e @returns {Promise} - */ - deleteClientAuthorization: function deleteClientAuthorization(clientId, uid) { - if (! clientId || ! uid) { - return P.reject(new Error('clientId and uid are required')); - } - - function deleteToken(tokens) { - const ids = Object.keys(tokens); - for (let i = 0; i < ids.length; i++) { - const id = ids[i]; - if (tokens[id].userId.toString('hex') === uid && - tokens[id].clientId.toString('hex') === clientId) { - delete tokens[id]; - } - } - } - - deleteToken(this.codes); - deleteToken(this.tokens); - deleteToken(this.refreshTokens); - - return P.resolve({}); - }, - generateRefreshToken: function generateRefreshToken(vals) { - var token = unique.token(); - var t = { - clientId: vals.clientId, - userId: vals.userId, - email: vals.email, - scope: vals.scope, - createdAt: new Date(), - lastUsedAt: new Date(), - token: encrypt.hash(token), - profileChangedAt: vals.profileChangedAt - }; - var ret = clone(t); - this.refreshTokens[unbuf(t.token)] = t; - ret.token = token; - return P.resolve(ret); - }, - getRefreshToken: function getRefreshToken(token) { - return P.resolve(clone(this.refreshTokens[unbuf(token)])); - }, - usedRefreshToken: function usedRefreshToken(token) { - if (! token) { - return P.reject(new Error('Update needs a token')); - } - var hex = unbuf(token); - if (! this.refreshTokens[hex]) { - return P.reject(new Error('Token does not exist')); - } - var old = this.refreshTokens[hex]; - old.lastUsedAt = new Date(); - - return P.resolve(old); - }, - - removeRefreshToken: function removeRefreshToken(id) { - delete this.refreshTokens[unbuf(id)]; - return P.resolve(); - }, - getEncodingInfo: function getEncodingInfo() { - console.warn('getEncodingInfo has no meaning with memory implementation'); // eslint-disable-line no-console - return P.resolve({}); - }, - removeUser: function removeUser(userId) { - deleteByUserId(this.tokens, userId); - deleteByUserId(this.refreshTokens, userId); - deleteByUserId(this.codes, userId); - return P.resolve(); - }, - /** - * Removes user's tokens and refreshTokens for canGrant and publicClient clients - * - * @param userId - * @returns {Promise} - */ - removePublicAndCanGrantTokens: function removePublicAndCanGrantTokens(userId) { - Object.keys(this.clients).forEach((clientId) => { - const client = this.clients[clientId]; - if (client.publicClient || client.canGrant) { - deleteByClientId(this.tokens, clientId); - deleteByClientId(this.refreshTokens, clientId); - } - }); - return P.resolve(); - }, - getScope: function getScope (scope) { - return P.resolve(this.scopes[scope]); - }, - registerScope: function registerScope (scope) { - this.scopes[scope.scope] = scope; - return P.resolve(); - }, - activateDeveloper: function activateDeveloper(email) { - var self = this; - - if (! email) { - return P.reject(new Error('Email is required')); - } - - return this.getDeveloper(email) - .then(function(result) { - if (result) { - return P.reject(new Error('ER_DUP_ENTRY')); - } - - var newId = unique.developerId(); - var developer = { - developerId: newId, - email: email, - createdAt: new Date() - }; - - self.developers[unbuf(newId)] = developer; - return developer; - - }); - }, - getDeveloper: function getDeveloper(email) { - var self = this; - var developer = null; - - if (! email) { - return P.reject(new Error('Email is required')); - } - - Object.keys(self.developers).forEach(function(developerId) { - var devEntry = self.developers[developerId]; - - if (devEntry.email === email) { - developer = devEntry; - } - }); - - return P.resolve(developer); - }, - removeDeveloper: function removeDeveloper(email) { - var self = this; - - if (! email) { - return P.reject(new Error('Email is required')); - } - - Object.keys(self.developers).forEach(function(developerId) { - var devEntry = self.developers[developerId]; - - if (devEntry.email === email) { - delete self.developers[developerId]; - } - }); - - return P.resolve(); - }, - developerOwnsClient: function devOwnsClient(developerEmail, clientId) { - var self = this; - var developerId; - - logger.debug('developerOwnsClient'); - return self.getDeveloper(developerEmail) - .then(function (developer) { - if (! developer) { - return P.reject(); - } - developerId = developer.developerId; - - return self.getClientDevelopers(clientId); - }) - .then(function (developers) { - - function hasDeveloper(developer) { - return unbuf(developer.developerId) === unbuf(developerId); - } - - if (developers.some(hasDeveloper)) { - return P.resolve(true); - } else { - return P.reject(false); - } - - }); - }, - registerClientDeveloper: function regClientDeveloper(developerId, clientId) { - var entry = { - developerId: buf(developerId), - clientId: buf(clientId), - createdAt: new Date() - }; - var uniqueHexId = unbuf(unique.id()); - - logger.debug('registerClientDeveloper', entry); - this.clientDevelopers[uniqueHexId] = entry; - return P.resolve(entry); - }, - getClientDevelopers: function getClientDevelopers(clientId) { - var self = this; - var developers = []; - - if (! clientId) { - return P.reject(new Error('Client id is required')); - } - - clientId = unbuf(clientId); - - Object.keys(self.clientDevelopers).forEach(function(key) { - var entry = self.clientDevelopers[key]; - - if (unbuf(entry.clientId) === clientId) { - developers.push(self.developers[unbuf(entry.developerId)]); - } - }); - - return P.resolve(developers); - } - -}; - -module.exports = MemoryStore; diff --git a/fxa-oauth-server/lib/db/mysql/index.js b/fxa-oauth-server/lib/db/mysql/index.js deleted file mode 100644 index 962710f..0000000 --- a/fxa-oauth-server/lib/db/mysql/index.js +++ /dev/null @@ -1,903 +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/. */ - -const path = require('path'); - -const buf = require('buf').hex; -const hex = require('buf').to.hex; -const moment = require('moment'); -const mysql = require('mysql'); -const MysqlPatcher = require('mysql-patcher'); - -const config = require('../../config'); -const encrypt = require('../../encrypt'); -const helpers = require('../helpers'); -const P = require('../../promise'); -const ScopeSet = require('fxa-shared').oauth.scopes; -const unique = require('../../unique'); -const patch = require('./patch'); - -const MAX_TTL = config.get('expiration.accessToken'); -const REQUIRED_SQL_MODES = [ - 'STRICT_ALL_TABLES', - 'NO_ENGINE_SUBSTITUTION' -]; -const REQUIRED_CHARSET = 'UTF8MB4_UNICODE_CI'; - -// logger is not const to support mocking in the unit tests -var logger = require('../../logging')('db.mysql'); - -function MysqlStore(options) { - if (options.charset && options.charset !== REQUIRED_CHARSET) { - logger.warn('createDatabase.invalidCharset', { charset: options.charset }); - throw new Error('You cannot use any charset besides ' + REQUIRED_CHARSET); - } else { - options.charset = REQUIRED_CHARSET; - } - options.typeCast = function(field, next) { - if (field.type === 'TINY' && field.length === 1) { - return field.string() === '1'; - } - return next(); - }; - logger.info('pool.create', { options: options }); - var pool = this._pool = mysql.createPool(options); - pool.on('enqueue', function() { - logger.info('pool.enqueue', { - queueLength: pool._connectionQueue && pool._connectionQueue.length - }); - }); -} - -// Apply patches up to the current patch level. -// This will also create the DB if it is missing. - -function updateDbSchema(patcher) { - logger.verbose('updateDbSchema', patcher.options); - - var d = P.defer(); - patcher.patch(function(err) { - if (err) { - logger.error('updateDbSchema', err); - return d.reject(err); - } - d.resolve(); - }); - - return d.promise; -} - -// Sanity-check that we're working with a compatible patch level. - -function checkDbPatchLevel(patcher) { - logger.verbose('checkDbPatchLevel', patcher.options); - - var d = P.defer(); - - patcher.readDbPatchLevel(function(err) { - if (err) { - return d.reject(err); - } - - // We can run if we're at or above some patch level. Should be - // equal at initial deployment, and may be one or more higher - // later on, due to database changes in preparation for the next - // release. - if (patcher.currentPatchLevel >= patch.level) { - return d.resolve(); - } - - err = 'unexpected db patch level: ' + patcher.currentPatchLevel; - return d.reject(new Error(err)); - }); - - return d.promise; -} - -MysqlStore.connect = function mysqlConnect(options) { - if (options.logger) { - logger = options.logger; - } - - options.createDatabase = options.createSchema; - options.dir = path.join(__dirname, 'patches'); - options.metaTable = 'dbMetadata'; - options.patchKey = 'schema-patch-level'; - options.patchLevel = patch.level; - options.mysql = mysql; - var patcher = new MysqlPatcher(options); - - return P.promisify(patcher.connect, {context: patcher})().then(function() { - if (options.createSchema) { - return updateDbSchema(patcher); - } - }).then(function() { - return checkDbPatchLevel(patcher); - }).catch(function(error) { - logger.error('checkDbPatchLevel', error); - throw error; - }).finally(function () { - return P.promisify(patcher.end, {context: patcher})(); - }).then(function() { - return new MysqlStore(options); - }); -}; - -const QUERY_CLIENT_REGISTER = - 'INSERT INTO clients ' + - '(id, name, imageUri, hashedSecret, hashedSecretPrevious, redirectUri,' + - 'trusted, allowedScopes, canGrant, publicClient) ' + - 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);'; -const QUERY_CLIENT_DEVELOPER_INSERT = - 'INSERT INTO clientDevelopers ' + - '(rowId, developerId, clientId) ' + - 'VALUES (?, ?, ?);'; -const QUERY_CLIENT_DEVELOPER_LIST_BY_CLIENT_ID = - 'SELECT developers.email, developers.createdAt ' + - 'FROM clientDevelopers, developers ' + - 'WHERE clientDevelopers.developerId = developers.developerId ' + - 'AND clientDevelopers.clientId=?;'; -const QUERY_DEVELOPER_OWNS_CLIENT = - 'SELECT clientDevelopers.rowId ' + - 'FROM clientDevelopers, developers ' + - 'WHERE developers.developerId = clientDevelopers.developerId ' + - 'AND developers.email =? AND clientDevelopers.clientId =?;'; -const QUERY_DEVELOPER_INSERT = - 'INSERT INTO developers ' + - '(developerId, email) ' + - 'VALUES (?, ?);'; -const QUERY_CLIENT_GET = 'SELECT * FROM clients WHERE id=?'; -const QUERY_CLIENT_LIST = 'SELECT id, name, redirectUri, imageUri, ' + - 'canGrant, publicClient, trusted ' + - 'FROM clients, clientDevelopers, developers ' + - 'WHERE clients.id = clientDevelopers.clientId AND ' + - 'developers.developerId = clientDevelopers.developerId AND ' + - 'developers.email =?;'; -const QUERY_PUBLIC_CLIENTS_LIST = 'SELECT * FROM clients WHERE publicClient = 1 OR canGrant = 1;'; -const QUERY_CLIENT_UPDATE = 'UPDATE clients SET ' + - 'name=COALESCE(?, name), imageUri=COALESCE(?, imageUri), ' + - 'hashedSecret=COALESCE(?, hashedSecret), ' + - 'hashedSecretPrevious=COALESCE(?, hashedSecretPrevious), ' + - 'redirectUri=COALESCE(?, redirectUri), ' + - 'trusted=COALESCE(?, trusted), allowedScopes=COALESCE(?, allowedScopes), ' + - 'canGrant=COALESCE(?, canGrant) ' + - 'WHERE id=?'; -// This query deletes everythin related to the client, and is thus quite expensive! -// Don't worry, it's not exposed to any production-facing routes. -const QUERY_CLIENT_DELETE = 'DELETE clients, codes, tokens, refreshTokens, clientDevelopers ' + - 'FROM clients ' + - 'LEFT JOIN codes ON clients.id = codes.clientId ' + - 'LEFT JOIN tokens ON clients.id = tokens.clientId ' + - 'LEFT JOIN refreshTokens ON clients.id = refreshTokens.clientId ' + - 'LEFT JOIN clientDevelopers ON clients.id = clientDevelopers.clientId ' + - 'WHERE clients.id=?'; -const QUERY_CODE_INSERT = - 'INSERT INTO codes (clientId, userId, email, scope, authAt, amr, aal, offline, code, codeChallengeMethod, codeChallenge, keysJwe, profileChangedAt) ' + - 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; -const QUERY_ACCESS_TOKEN_INSERT = - 'INSERT INTO tokens (clientId, userId, email, scope, type, expiresAt, ' + - 'token, profileChangedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'; -const QUERY_REFRESH_TOKEN_INSERT = - 'INSERT INTO refreshTokens (clientId, userId, email, scope, token, profileChangedAt) VALUES ' + - '(?, ?, ?, ?, ?, ?)'; -const QUERY_ACCESS_TOKEN_FIND = 'SELECT * FROM tokens WHERE token=?'; -const QUERY_REFRESH_TOKEN_FIND = 'SELECT * FROM refreshTokens where token=?'; -const QUERY_REFRESH_TOKEN_LAST_USED_UPDATE = 'UPDATE refreshTokens SET lastUsedAt=? WHERE token=?'; -const QUERY_CODE_FIND = 'SELECT * FROM codes WHERE code=?'; -const QUERY_CODE_DELETE = 'DELETE FROM codes WHERE code=?'; -const QUERY_ACCESS_TOKEN_DELETE = 'DELETE FROM tokens WHERE token=?'; -const QUERY_REFRESH_TOKEN_DELETE = 'DELETE FROM refreshTokens WHERE token=?'; -const QUERY_ACCESS_TOKEN_DELETE_USER = 'DELETE FROM tokens WHERE userId=?'; - -const QUERY_DELETE_ACCESS_TOKEN_FOR_PUBLIC_CLIENTS = 'DELETE FROM tokens WHERE userId=? AND clientId IN (?);'; -const QUERY_DELETE_REFRESH_TOKEN_FOR_PUBLIC_CLIENTS = 'DELETE FROM refreshTokens WHERE userId=? AND clientId IN (?);'; -const QUERY_REFRESH_TOKEN_DELETE_USER = - 'DELETE FROM refreshTokens WHERE userId=?'; -const QUERY_CODE_DELETE_USER = 'DELETE FROM codes WHERE userId=?'; -const QUERY_DEVELOPER = 'SELECT * FROM developers WHERE email=?'; -const QUERY_DEVELOPER_DELETE = 'DELETE developers, clientDevelopers ' + - 'FROM developers ' + - 'LEFT JOIN clientDevelopers ON developers.developerId = clientDevelopers.developerId ' + - 'WHERE developers.email=?'; -const QUERY_PURGE_EXPIRED_TOKENS = 'DELETE FROM tokens WHERE clientId NOT IN (?) AND expiresAt < NOW() LIMIT ?;'; -const QUERY_EXPIRED_TOKENS = - 'SELECT expiresAt, token, clientId FROM tokens WHERE expiresAt >= ? AND expiresAt <= NOW() ORDER BY expiresAt ASC LIMIT ?'; -const QUERY_DELETE_EXPIRED_TOKENS = 'DELETE FROM tokens WHERE token IN (?) AND clientId NOT IN (?) AND expiresAt <= NOW()'; -const QUERY_LAST_PURGE_TIME = 'SELECT value FROM dbMetadata WHERE name = "last-purge-time"'; -const QUERY_REPLACE_LAST_PURGE_TIME = 'REPLACE INTO dbMetadata (name, value) VALUES ("last-purge-time", ?)'; -// Token management by uid. -// Returns the most recent token used with a client name and client id. -// Does not include clients that canGrant. -const QUERY_ACTIVE_CLIENT_TOKENS_BY_UID = - 'SELECT tokens.clientId AS id, tokens.createdAt, tokens.scope, clients.name ' + - 'FROM tokens LEFT OUTER JOIN clients ON clients.id = tokens.clientId ' + - 'WHERE tokens.userId=? AND tokens.expiresAt > NOW() AND clients.canGrant = 0;'; -const DELETE_ACTIVE_CODES_BY_CLIENT_AND_UID = - 'DELETE FROM codes WHERE clientId=? AND userId=?'; -const DELETE_ACTIVE_TOKENS_BY_CLIENT_AND_UID = - 'DELETE FROM tokens WHERE clientId=? AND userId=?'; -const DELETE_ACTIVE_REFRESH_TOKENS_BY_CLIENT_AND_UID = - 'DELETE FROM refreshTokens WHERE clientId=? AND userId=?'; -// Scope queries -const QUERY_SCOPE_FIND = - 'SELECT * ' + - 'FROM scopes ' + - 'WHERE scopes.scope=?;'; -const QUERY_SCOPES_INSERT = - 'INSERT INTO scopes (scope, hasScopedKeys) ' + - 'VALUES (?, ?);'; - -function firstRow(rows) { - return rows[0]; -} - -function releaseConn(connection) { - connection.release(); -} - -MysqlStore.prototype = { - - ping: function ping() { - logger.debug('ping'); - // see bluebird.using(): - // https://github.com/petkaantonov/bluebird/blob/master/API.md#resource-management - return P.using(this._getConnection(), function(conn) { - return new P(function(resolve, reject) { - conn.ping(function(err) { - if (err) { - logger.error('ping:', err); - reject(err); - } else { - resolve({}); - } - }); - }); - }); - }, - - // createdAt is DEFAULT NOW() in the schema.sql - registerClient: function registerClient(client) { - var id; - if (client.id) { - id = buf(client.id); - } else { - id = unique.id(); - } - logger.debug('registerClient', { name: client.name, id: hex(id) }); - return this._write(QUERY_CLIENT_REGISTER, [ - id, - client.name, - client.imageUri || '', - buf(client.hashedSecret), - client.hashedSecretPrevious ? buf(client.hashedSecretPrevious) : null, - client.redirectUri, - !! client.trusted, - client.allowedScopes ? client.allowedScopes : null, - !! client.canGrant, - !! client.publicClient - ]).then(function() { - logger.debug('registerClient.success', { id: hex(id) }); - client.id = id; - return client; - }); - }, - registerClientDeveloper: function regClientDeveloper(developerId, clientId) { - if (! developerId || ! clientId) { - var err = new Error('Owner registration requires user and developer id'); - return P.reject(err); - } - - var rowId = unique.id(); - - logger.debug('registerClientDeveloper', { - rowId: rowId, - developerId: developerId, - clientId: clientId - }); - - return this._write(QUERY_CLIENT_DEVELOPER_INSERT, [ - buf(rowId), - buf(developerId), - buf(clientId) - ]); - }, - getClientDevelopers: function getClientDevelopers (clientId) { - if (! clientId) { - return P.reject(new Error('Client id is required')); - } - return this._read(QUERY_CLIENT_DEVELOPER_LIST_BY_CLIENT_ID, [ - buf(clientId) - ]); - }, - activateDeveloper: function activateDeveloper(email) { - if (! email) { - return P.reject(new Error('Email is required')); - } - - var developerId = unique.developerId(); - logger.debug('activateDeveloper', { developerId: developerId }); - return this._write(QUERY_DEVELOPER_INSERT, [ - developerId, email - ]).then(function () { - return this.getDeveloper(email); - }.bind(this)); - }, - getDeveloper: function(email) { - if (! email) { - return P.reject(new Error('Email is required')); - } - - return this._readOne(QUERY_DEVELOPER, [ - email - ]); - }, - removeDeveloper: function(email) { - if (! email) { - return P.reject(new Error('Email is required')); - } - - return this._write(QUERY_DEVELOPER_DELETE, [ - email - ]); - }, - developerOwnsClient: function devOwnsClient(developerEmail, clientId) { - return this._readOne(QUERY_DEVELOPER_OWNS_CLIENT, [ - developerEmail, buf(clientId) - ]).then(function(result) { - if (result) { - return P.resolve(true); - } else { - return P.reject(false); - } - }); - }, - updateClient: function updateClient(client) { - if (! client.id) { - return P.reject(new Error('Update client needs an id')); - } - var secret = client.hashedSecret; - if (secret) { - secret = buf(secret); - } - - var secretPrevious = client.hashedSecretPrevious; - if (secretPrevious) { - secretPrevious = buf(secretPrevious); - } - return this._write(QUERY_CLIENT_UPDATE, [ - // VALUES - client.name, - client.imageUri, - secret, - secretPrevious, - client.redirectUri, - client.trusted, - client.allowedScopes, - client.canGrant, - - // WHERE - buf(client.id) - ]); - }, - - getClient: function getClient(id) { - return this._readOne(QUERY_CLIENT_GET, [buf(id)]); - }, - getClients: function getClients(email) { - return this._read(QUERY_CLIENT_LIST, [ email ]); - }, - removeClient: function removeClient(id) { - return this._write(QUERY_CLIENT_DELETE, [buf(id)]); - }, - generateCode: function generateCode(codeObj) { - var code = unique.code(); - var hash = encrypt.hash(code); - return this._write(QUERY_CODE_INSERT, [ - codeObj.clientId, - codeObj.userId, - codeObj.email, - codeObj.scope.toString(), - codeObj.authAt, - codeObj.amr ? codeObj.amr.join(',') : null, - codeObj.aal || null, - !! codeObj.offline, - hash, - codeObj.codeChallengeMethod, - codeObj.codeChallenge, - codeObj.keysJwe, - codeObj.profileChangedAt - ]).then(function() { - return code; - }); - }, - getCode: function getCode(code) { - var hash = encrypt.hash(code); - return this._readOne(QUERY_CODE_FIND, [hash]).then(function(code) { - if (code) { - code.scope = ScopeSet.fromString(code.scope); - if (code.amr !== null) { - code.amr = code.amr.split(','); - } - } - return code; - }); - }, - removeCode: function removeCode(code) { - var hash = encrypt.hash(code); - return this._write(QUERY_CODE_DELETE, [hash]); - }, - generateAccessToken: function generateAccessToken(vals) { - var t = { - clientId: buf(vals.clientId), - userId: buf(vals.userId), - email: vals.email, - scope: vals.scope, - token: unique.token(), - type: 'bearer', - expiresAt: vals.expiresAt || new Date(Date.now() + (vals.ttl * 1000 || MAX_TTL)), - profileChangedAt: vals.profileChangedAt || 0 - }; - return this._write(QUERY_ACCESS_TOKEN_INSERT, [ - t.clientId, - t.userId, - t.email, - t.scope.toString(), - t.type, - t.expiresAt, - encrypt.hash(t.token), - t.profileChangedAt, - ]).then(function() { - return t; - }); - }, - - /** - * Get an access token by token id - * @param id Token Id - * @returns {*} - */ - getAccessToken: function getAccessToken(id) { - return this._readOne(QUERY_ACCESS_TOKEN_FIND, [buf(id)]).then(function(t) { - if (t) { - t.scope = ScopeSet.fromString(t.scope); - } - return t; - }); - }, - - /** - * Remove token by token id - * @param id - * @returns {*} - */ - removeAccessToken: function removeAccessToken(id) { - return this._write(QUERY_ACCESS_TOKEN_DELETE, [buf(id)]); - }, - - /** - * Get all services that have have non-expired tokens - * @param {String} uid User ID as hex - * @returns {Promise} - */ - getActiveClientsByUid: function getActiveClientsByUid(uid) { - return this._read(QUERY_ACTIVE_CLIENT_TOKENS_BY_UID, [ - buf(uid) - ]).then(function(activeClientTokens) { - activeClientTokens.forEach(t => { - t.scope = ScopeSet.fromString(t.scope); - }); - return helpers.aggregateActiveClients(activeClientTokens); - }); - }, - - /** - * Delete all authorization grants for some clientId and uid. - * - * @param {String} clientId Client ID - * @param {String} uid User Id as Hex - * @returns {Promise} - */ - deleteClientAuthorization: function deleteClientAuthorization(clientId, uid) { - const deleteCodes = this._write(DELETE_ACTIVE_CODES_BY_CLIENT_AND_UID, [ - buf(clientId), - buf(uid) - ]); - - const deleteTokens = this._write(DELETE_ACTIVE_TOKENS_BY_CLIENT_AND_UID, [ - buf(clientId), - buf(uid) - ]); - - const deleteRefreshTokens = this._write(DELETE_ACTIVE_REFRESH_TOKENS_BY_CLIENT_AND_UID, [ - buf(clientId), - buf(uid) - ]); - - return P.all([ - deleteCodes, - deleteTokens, - deleteRefreshTokens - ]); - }, - - generateRefreshToken: function generateRefreshToken(vals) { - var t = { - clientId: vals.clientId, - userId: vals.userId, - email: vals.email, - scope: vals.scope, - profileChangedAt: vals.profileChangedAt - }; - var token = unique.token(); - var hash = encrypt.hash(token); - return this._write(QUERY_REFRESH_TOKEN_INSERT, [ - t.clientId, - t.userId, - t.email, - t.scope.toString(), - hash, - t.profileChangedAt - ]).then(function() { - t.token = token; - return t; - }); - }, - - getRefreshToken: function getRefreshToken(token) { - return this._readOne(QUERY_REFRESH_TOKEN_FIND, [buf(token)]) - .then(function(t) { - if (t) { - t.scope = ScopeSet.fromString(t.scope); - } - return t; - }); - }, - - usedRefreshToken: function usedRefreshToken(token) { - var now = new Date(); - return this._write(QUERY_REFRESH_TOKEN_LAST_USED_UPDATE, [ - now, - // WHERE - token - ]); - }, - - removeRefreshToken: function removeRefreshToken(id) { - return this._write(QUERY_REFRESH_TOKEN_DELETE, [buf(id)]); - }, - - getEncodingInfo: function getEncodingInfo() { - var info = {}; - - var self = this; - var qry = 'SHOW VARIABLES LIKE "%character\\_set\\_%"'; - return this._read(qry).then(function(rows) { - rows.forEach(function(row) { - info[row.Variable_name] = row.Value; - }); - - qry = 'SHOW VARIABLES LIKE "%collation\\_%"'; - return self._read(qry).then(function(rows) { - rows.forEach(function(row) { - info[row.Variable_name] = row.Value; - }); - return info; - }); - }); - }, - - purgeExpiredTokens: function purgeExpiredTokens(numberOfTokens, - delaySeconds, - ignoreClientId, - deleteBatchSize = 200) - { - const self = this; - if (! ignoreClientId) { - throw new Error('empty ignoreClientId'); - } - - if (! Array.isArray(ignoreClientId)) { - ignoreClientId = [ ignoreClientId ]; - } - - const clientIds = ignoreClientId.map((id) => { - return self.getClient(id); - }); - - return P.all(clientIds) - .then((results) => { - // This ensures that purgeExpiredTokens can not be called with an - // unknown ignoreClientId(s). - results.forEach((ignoreClient) => { - if (! ignoreClient) { - throw new Error('unknown ignoreClientId ' + ignoreClientId); - } - }); - }) - .then(() => { - if (numberOfTokens <= deleteBatchSize) { - deleteBatchSize = numberOfTokens; - } - - let deletedItems = 0; - const promiseWhile = P.method(() => { - if (deletedItems >= numberOfTokens) { - const message = 'deletedItems >= numberOfTokens'; - logger.info('purgeExpiredTokens', { - message: message, - deletedItems: deletedItems, - numberOfTokens: numberOfTokens, - deleteBatchSize: deleteBatchSize - }); - return; - } - - const clientIn = ignoreClientId.map((id) => { - return buf(id); - }); - - return self._write(QUERY_PURGE_EXPIRED_TOKENS, [clientIn, deleteBatchSize]) - .then((res) => { - logger.info('purgeExpiredTokens', { affectedRows: res.affectedRows }); - - // Break loop if no items were effected by delete. - // All expired tokens have been deleted. - if (res.affectedRows === 0) { - const message = '0 affectedRows. Bailing out.'; - logger.info('purgeExpiredTokens', { message: message }); - return; - } - - deletedItems = deletedItems + res.affectedRows; - - return P.delay(delaySeconds * 1000) - .then(() => { - return promiseWhile(); - }); - }); - }); - - return promiseWhile(); - }) - .then(() => { - logger.info('purgeExpiredTokens', { message: 'completed' }); - }); - }, - - - // This version of purgeExpiredTokens uses the strategy of selecting a set - // of tokens to delete and then issuing deletes by primary key. - purgeExpiredTokensById: function purgeExpiredTokensById(numberOfTokens, - delaySeconds, - ignoreClientId, - deleteBatchSize = 200) - { - const self = this; - if (! ignoreClientId) { - throw new Error('empty ignoreClientId'); - } - - if (! Array.isArray(ignoreClientId)) { - ignoreClientId = [ ignoreClientId ]; - } - - if (numberOfTokens <= deleteBatchSize) { - deleteBatchSize = numberOfTokens; - } - - const clientIds = ignoreClientId.map((id) => { - return self.getClient(id); - }); - - let lastPurgeTime; - - return P.all(clientIds) - .then((results) => { - // This ensures that purgeExpiredTokensById can not be called with an - // unknown ignoreClientId(s). - results.forEach((ignoreClient) => { - if (! ignoreClient) { - throw new Error('unknown ignoreClientId ' + ignoreClientId); - } - }); - }) - .then(() => { - // Continue from the last recorded 'last-purge-time', if available. - return self._readOne(QUERY_LAST_PURGE_TIME) - .then((res) => { - const OLDEST_POSSIBLE_TOKEN_EXPIRY = '2015-03-01 00:00:00'; - lastPurgeTime = (res && res.value) || OLDEST_POSSIBLE_TOKEN_EXPIRY; - logger.info('purgeExpiredTokensById', { lastPurgeTime: lastPurgeTime }); - }); - }) - .then(() => { - let deletedItems = 0; - const promiseWhile = P.method(() => { - if (deletedItems >= numberOfTokens) { - const message = 'deletedItems >= numberOfTokens'; - logger.info('purgeExpiredTokensById', { - message: message, - deletedItems: deletedItems, - numberOfTokens: numberOfTokens, - deleteBatchSize: deleteBatchSize - }); - return; - } - - const clientIn = ignoreClientId.map((id) => { - return buf(id); - }); - - return self._read(QUERY_EXPIRED_TOKENS, [lastPurgeTime, deleteBatchSize]) - .then((res) => { - logger.info('purgeExpiredTokensById', { rowsReturned: res.length }); - - const tokensForDeletion = res.filter((row) => { - const expiresAt = moment(row.expiresAt).format('YYYY-MM-DD HH:mm:ss'); - if (expiresAt > lastPurgeTime) { - lastPurgeTime = expiresAt; - } - - if (ignoreClientId.includes(hex(row.clientId))) { - return false; - } - - return true; - }).map((row) => row.token); - - // Break loop if we have no candidate rows to delete. - if (tokensForDeletion.length === 0) { - const message = '0 tokensForDeletion. Bailing out.'; - logger.info('purgeExpiredTokensById', { message: message }); - return; - } - - logger.info('purgeExpiredTokensById', { tokensForDeletion: tokensForDeletion.length, lastPurgeTime: lastPurgeTime }); - - return self._write(QUERY_DELETE_EXPIRED_TOKENS, [ tokensForDeletion, clientIn ]) - .then((res) => { - logger.info('purgeExpiredTokensById', { affectedRows: res.affectedRows }); - - // Break loop if no items were effected by delete. - // All expired tokens have been deleted. - if (res.affectedRows === 0) { - const message = '0 affectedRows. Bailing out.'; - logger.info('purgeExpiredTokensById', { message: message }); - return; - } - - deletedItems = deletedItems + res.affectedRows; - - logger.info('purgeExpiredTokensById', { lastPurgeTime: lastPurgeTime }); - // Update 'last-purge-time' and schedule next iteration. - return self._write(QUERY_REPLACE_LAST_PURGE_TIME, [ lastPurgeTime ]) - .delay(delaySeconds * 1000) - .then(() => { - return promiseWhile(); - }); - }); - }); - }); - - return promiseWhile(); - }) - .then(() => { - logger.info('purgeExpiredTokensById', { message: 'completed' }); - }); - }, - - removeUser: function removeUser(userId) { - // TODO this should be a transaction or stored procedure - var id = buf(userId); - return this._write(QUERY_ACCESS_TOKEN_DELETE_USER, [id]) - .then(this._write.bind(this, QUERY_REFRESH_TOKEN_DELETE_USER, [id])) - .then(this._write.bind(this, QUERY_CODE_DELETE_USER, [id])); - }, - - /** - * Removes user's tokens and refreshTokens for canGrant and publicClient clients - * - * @param userId - * @returns {Promise} - */ - removePublicAndCanGrantTokens: function removePublicAndCanGrantTokens(userId) { - const uid = buf(userId); - - return this._read(QUERY_PUBLIC_CLIENTS_LIST).then((_clients) => { - const clientIds = _clients.map((client) => client.id); - - return this._write(QUERY_DELETE_ACCESS_TOKEN_FOR_PUBLIC_CLIENTS, [uid, clientIds]) - .then(() => this._write(QUERY_DELETE_REFRESH_TOKEN_FOR_PUBLIC_CLIENTS, [uid, clientIds])); - }); - }, - - getScope: function getScope (scope) { - return this._readOne(QUERY_SCOPE_FIND, [scope]); - }, - - registerScope: function registerScope (scope) { - return this._write(QUERY_SCOPES_INSERT, [ - scope.scope, - scope.hasScopedKeys - ]); - }, - - _write: function _write(sql, params) { - return this._query(sql, params); - }, - - _read: function _read(sql, params) { - return this._query(sql, params); - }, - - _readOne: function _readOne(sql, params) { - return this._read(sql, params).then(firstRow); - }, - - _getConnection: function _getConnection() { - // see bluebird.using()/disposer(): - // https://github.com/petkaantonov/bluebird/blob/master/API.md#resource-management - // - // tl;dr: using() and disposer() ensures that the dispose method will - // ALWAYS be called at the end of the promise stack, regardless of - // various errors thrown. So this should ALWAYS release the connection. - var pool = this._pool; - return new P(function(resolve, reject) { - pool.getConnection(function(err, conn) { - if (err) { - return reject(err); - } - - if (conn._fxa_initialized) { - return resolve(conn); - } - // Enforce sane defaults on every new connection. - // These *should* be set by the database by default, but it's nice - // to have an additional layer of protection here. - conn.query('SELECT @@sql_mode AS mode', function(err, rows) { - if (err) { - return reject(err); - } - var modes = rows[0]['mode'].split(','); - var needToSetMode = false; - REQUIRED_SQL_MODES.forEach(function(requiredMode) { - if (modes.indexOf(requiredMode) === -1) { - modes.push(requiredMode); - needToSetMode = true; - } - }); - if (! needToSetMode) { - conn._fxa_initialized = true; - return resolve(conn); - } - var mode = modes.join(','); - conn.query('SET SESSION sql_mode = \'' + mode + '\'', function(err) { - if (err) { - return reject(err); - } - conn._fxa_initialized = true; - return resolve(conn); - }); - }); - - }); - }).disposer(releaseConn); - }, - - _query: function _query(sql, params) { - return P.using(this._getConnection(), function(conn) { - return new P(function(resolve, reject) { - conn.query(sql, params || [], function(err, results) { - if (err) { - reject(err); - } else { - resolve(results); - } - }); - }); - }); - } -}; - -module.exports = MysqlStore; diff --git a/fxa-oauth-server/lib/db/mysql/patch.js b/fxa-oauth-server/lib/db/mysql/patch.js deleted file mode 100644 index c244fe7..0000000 --- a/fxa-oauth-server/lib/db/mysql/patch.js +++ /dev/null @@ -1,9 +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/. */ - -// The expected patch level of the database. -// Update this if you add a new patch, and don't forget to update -// the documentation for the current schema in ../schema.sql. - -module.exports.level = 23; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-000-001.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-000-001.sql deleted file mode 100644 index b5d5c19..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-000-001.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Create the 'dbMetadata' table. --- Note: This should be the only thing in this initial patch. - -CREATE TABLE dbMetadata ( - name VARCHAR(255) NOT NULL PRIMARY KEY, - value VARCHAR(255) NOT NULL -) ENGINE=InnoDB; - -INSERT INTO dbMetadata SET name = 'schema-patch-level', value = '1'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-001-000.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-001-000.sql deleted file mode 100644 index 1003360..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-001-000.sql +++ /dev/null @@ -1,2 +0,0 @@ --- -- drop the dbMetadata table --- DROP TABLE dbMetadata; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-001-002.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-001-002.sql deleted file mode 100644 index 3925d15..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-001-002.sql +++ /dev/null @@ -1,45 +0,0 @@ --- Create the initial set of tables. --- --- Since this is the first migration, we use `IF NOT EXISTS` to allow us --- to run this on a db that already has the original schema in place. The --- patch will then be a no-op. Subsequent patches should *not* use `IF --- NOT EXISTS` but should fail noisily if the db is in an unexpected state. - -CREATE TABLE IF NOT EXISTS clients ( - id BINARY(8) PRIMARY KEY, - secret BINARY(32) NOT NULL, - name VARCHAR(256) NOT NULL, - imageUri VARCHAR(256) NOT NULL, - redirectUri VARCHAR(256) NOT NULL, - whitelisted BOOLEAN DEFAULT FALSE, - canGrant BOOLEAN DEFAULT FALSE, - createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci; - -CREATE TABLE IF NOT EXISTS codes ( - code BINARY(32) PRIMARY KEY, - clientId BINARY(8) NOT NULL, - INDEX codes_client_id(clientId), - FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE, - userId BINARY(16) NOT NULL, - INDEX codes_user_id(userId), - email VARCHAR(256) NOT NULL, - scope VARCHAR(256) NOT NULL, - createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci; - -CREATE TABLE IF NOT EXISTS tokens ( - token BINARY(32) PRIMARY KEY, - clientId BINARY(8) NOT NULL, - INDEX tokens_client_id(clientId), - FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE, - userId BINARY(16) NOT NULL, - INDEX tokens_user_id(userId), - email VARCHAR(256) NOT NULL, - type VARCHAR(16) NOT NULL, - scope VARCHAR(256) NOT NULL, - createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci; - -UPDATE dbMetadata SET value = '2' WHERE name = 'schema-patch-level'; - diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-002-001.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-002-001.sql deleted file mode 100644 index e55cc66..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-002-001.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Drop all the tables. --- (commented out to avoid accidentally running this in production...) - --- DROP TABLE clients; --- DROP TABLE codes; --- DROP TABLE tokens; - --- UPDATE dbMetadata SET value = '1' WHERE name = 'schema-patch-level'; - diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-002-003.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-002-003.sql deleted file mode 100644 index b5cdd01..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-002-003.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Add `authAt` column to the `codes` table. - -ALTER TABLE codes ADD COLUMN authAt BIGINT DEFAULT 0; - -UPDATE dbMetadata SET value = '3' WHERE name = 'schema-patch-level'; - diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-003-002.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-003-002.sql deleted file mode 100644 index 7d19ecc..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-003-002.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Remove `authAt` column from the `codes` table. --- (commented out to avoid accidentally running this in production...) - --- ALTER TABLE codes DROP COLUMN authAt; - --- UPDATE dbMetadata SET value = '2' WHERE name = 'schema-patch-level'; - diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-003-004.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-003-004.sql deleted file mode 100644 index 0cc4495..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-003-004.sql +++ /dev/null @@ -1,19 +0,0 @@ --- Adds support for Client Developers for OAuth clients - -CREATE TABLE developers ( - developerId BINARY(16) NOT NULL PRIMARY KEY, - email VARCHAR(255) NOT NULL, - createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE(email) -) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci; - -CREATE TABLE clientDevelopers ( - rowId BINARY(8) NOT NULL PRIMARY KEY, - developerId BINARY(16) NOT NULL, - FOREIGN KEY (developerId) REFERENCES developers(developerId) ON DELETE CASCADE, - clientId BINARY(8) NOT NULL, - FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE, - createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci; - -UPDATE dbMetadata SET value = '4' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-004-003.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-004-003.sql deleted file mode 100644 index 8dbde09..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-004-003.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Remove support for Client Developers for OAuth clients - --- DROP TABLE clientDevelopers; --- DROP TABLE developers; - --- UPDATE dbMetadata SET value = '3' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-004-005.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-004-005.sql deleted file mode 100644 index cfc632f..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-004-005.sql +++ /dev/null @@ -1,13 +0,0 @@ --- Begins process of renaming "whitelisted" to "trusted". --- We need to drop the "whitelisted" column in a separate patch in order --- to safely deploy this without downtime. - -ALTER TABLE clients ADD COLUMN trusted BOOLEAN DEFAULT FALSE; -UPDATE clients SET trusted=whitelisted; - --- Adds new "termsUri" and "privacyUri" columns for third-party clients. - -ALTER TABLE clients ADD COLUMN termsUri VARCHAR(256) NOT NULL AFTER redirectUri; -ALTER TABLE clients ADD COLUMN privacyUri VARCHAR(256) NOT NULL AFTER termsUri; - -UPDATE dbMetadata SET value = '5' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-005-004.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-005-004.sql deleted file mode 100644 index d11a3e3..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-005-004.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Remove "termsUri" and "privacyUri" columns". --- Remove "trusted" column, ensuring to sync with old "whitelist" column. - --- ALTER TABLE clients DROP COLUMN privacyUri; --- ALTER TABLE clients DROP COLUMN termsUri; --- UPDATE clients SET whitelisted=trusted; --- ALTER TABLE clients DROP COLUMN trusted; - --- UPDATE dbMetadata SET value = '4' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-005-006.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-005-006.sql deleted file mode 100644 index 8da3da7..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-005-006.sql +++ /dev/null @@ -1,24 +0,0 @@ --- Add refreshTokens - -CREATE TABLE refreshTokens ( - token BINARY(32) PRIMARY KEY, - clientId BINARY(8) NOT NULL, - INDEX tokens_client_id(clientId), - FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE, - userId BINARY(16) NOT NULL, - INDEX tokens_user_id(userId), - email VARCHAR(256) NOT NULL, - scope VARCHAR(256) NOT NULL, - createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci; - --- Add expiresAt column for access tokens - -ALTER TABLE tokens ADD COLUMN expiresAt TIMESTAMP NOT NULL DEFAULT "1980-01-01 00:00:00"; -UPDATE tokens SET expiresAt = DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 2 WEEK); - --- Add offline column to codes - -ALTER TABLE codes ADD COLUMN offline BOOLEAN NOT NULL DEFAULT FALSE; - -UPDATE dbMetadata SET value = '6' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-006-005.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-006-005.sql deleted file mode 100644 index 66f6f70..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-006-005.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Remove refreshToken table and expiresAt column from tokens table. --- (commented out to avoid accidentally running this in production...) - --- DROP TABLE refreshTokens; --- ALTER TABLE tokens DROP COLUMN expiresAt; --- ALTER TABLE codes DROP COLUMN offline; - --- UPDATE dbMetadata SET value = '5' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-006-007.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-006-007.sql deleted file mode 100644 index f56d112..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-006-007.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Change clients.secret to clients.hashedSecret --- Drop whitelisted column - -ALTER TABLE clients CHANGE secret hashedSecret BINARY(32); -ALTER TABLE clients DROP COLUMN whitelisted; - -UPDATE dbMetadata SET value = '7' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-007-006.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-007-006.sql deleted file mode 100644 index efdeacf..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-007-006.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Change clients.hashedSecret to clients.secret --- (commented out to avoid accidentally running this in production...) - --- ALTER TABLE clients CHANGE hashedSecret secret BINARY(32); --- ALTER TABLE clients ADD COLUMN whitelisted BOOLEAN DEFAULT FALSE; --- UPDATE clients SET whitelisted=trusted; - --- UPDATE dbMetadata SET value = '6' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-007-008.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-007-008.sql deleted file mode 100644 index 4095853..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-007-008.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE clients CHANGE hashedSecret secret BINARY(32); -ALTER TABLE clients ADD COLUMN whitelisted BOOLEAN DEFAULT FALSE; -UPDATE clients SET whitelisted=trusted; - -UPDATE dbMetadata SET value = '8' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-008-007.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-008-007.sql deleted file mode 100644 index 23d6d2b..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-008-007.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Change clients.secret to clients.hashedSecret --- Drop whitelisted column --- (commented out to avoid accidentally running this in production...) - --- ALTER TABLE clients CHANGE secret hashedSecret BINARY(32); --- ALTER TABLE clients DROP COLUMN whitelisted; - --- UPDATE dbMetadata SET value = '7' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-008-009.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-008-009.sql deleted file mode 100644 index 7160e00..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-008-009.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Add hashedSecret column, to replace secret column. - -ALTER TABLE clients ADD COLUMN hashedSecret BINARY(32); -UPDATE clients SET hashedSecret = secret; - -UPDATE dbMetadata SET value = '9' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-009-008.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-009-008.sql deleted file mode 100644 index 262b97d..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-009-008.sql +++ /dev/null @@ -1,4 +0,0 @@ --- (commented out to avoid accidentally running this in production...) - --- ALTER TABLE clients DROP COLUMN hashedSecret; --- UPDATE dbMetadata SET value = '8' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-009-010.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-009-010.sql deleted file mode 100644 index d61217c..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-009-010.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Remove `secret` column. - -ALTER TABLE clients DROP COLUMN secret; - -UPDATE dbMetadata SET value = '10' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-010-009.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-010-009.sql deleted file mode 100644 index b85f074..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-010-009.sql +++ /dev/null @@ -1,6 +0,0 @@ --- (commented out to avoid accidentally running this in production...) - --- ALTER TABLE clients ADD COLUMN secret BINARY(32); --- UPDATE clients SET secret = hashedSecret; - --- UPDATE dbMetadata SET value = '9' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-010-011.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-010-011.sql deleted file mode 100644 index efddf0a..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-010-011.sql +++ /dev/null @@ -1,5 +0,0 @@ --- dropping NOT NULL constraint - -ALTER TABLE tokens MODIFY COLUMN email VARCHAR(256); - -UPDATE dbMetadata SET value = '11' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-011-010.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-011-010.sql deleted file mode 100644 index 07a177e..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-011-010.sql +++ /dev/null @@ -1,5 +0,0 @@ --- (commented out to avoid accidentally running this in production...) - --- ALTER TABLE tokens MODIFY COLUMN email VARCHAR(256) NOT NULL; - --- UPDATE dbMetadata SET value = '10' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-011-012.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-011-012.sql deleted file mode 100644 index 5a8321c..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-011-012.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Drop whitelisted column - -ALTER TABLE clients DROP COLUMN whitelisted; - -UPDATE dbMetadata SET value = '12' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-012-011.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-012-011.sql deleted file mode 100644 index 36c8206..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-012-011.sql +++ /dev/null @@ -1,6 +0,0 @@ --- (commented out to avoid accidentally running this in production...) - --- ALTER TABLE clients ADD COLUMN whitelisted BOOLEAN DEFAULT FALSE; --- UPDATE clients SET whitelisted=trusted; - --- UPDATE dbMetadata SET value = '11' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-012-013.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-012-013.sql deleted file mode 100644 index 8fa4e1e..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-012-013.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Re-create NOT NULL constraint on the email column. --- We weren't able to drop it in production, this migration --- brings it back in our dev environments. - -ALTER TABLE tokens MODIFY COLUMN email VARCHAR(256) NOT NULL; - -UPDATE dbMetadata SET value = '13' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-013-012.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-013-012.sql deleted file mode 100644 index 28c111a..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-013-012.sql +++ /dev/null @@ -1,5 +0,0 @@ --- (commented out to avoid accidentally running this in production...) - --- ALTER TABLE tokens MODIFY COLUMN email VARCHAR(256); - --- UPDATE dbMetadata SET value = '12' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-013-014.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-013-014.sql deleted file mode 100644 index d9ac37a..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-013-014.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Add hashedSecretPrevious column - -ALTER TABLE clients ADD COLUMN hashedSecretPrevious BINARY(32); - -UPDATE dbMetadata SET value = '14' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-014-013.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-014-013.sql deleted file mode 100644 index 5139313..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-014-013.sql +++ /dev/null @@ -1,5 +0,0 @@ --- (commented out to avoid accidentally running this in production...) - --- ALTER TABLE clients DROP COLUMN hashedSecretPrevious; - --- UPDATE dbMetadata SET value = '13' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-014-015.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-014-015.sql deleted file mode 100644 index c625915..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-014-015.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Add index idx_expiresAt to token table - -ALTER TABLE tokens ADD INDEX idx_expiresAt (expiresAt), ALGORITHM = INPLACE, LOCK = NONE; - -UPDATE dbMetadata SET value = '15' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-015-014.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-015-014.sql deleted file mode 100644 index 8db94cc..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-015-014.sql +++ /dev/null @@ -1,5 +0,0 @@ --- (commented out to avoid accidentally running this in production...) - --- ALTER TABLE tokens DROP INDEX idx_expiresAt; - --- UPDATE dbMetadata SET value = '14' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-015-016.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-015-016.sql deleted file mode 100644 index 8fb1f20..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-015-016.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Remove "termsUri" and "privacyUri" columns". - -ALTER TABLE clients DROP COLUMN privacyUri; -ALTER TABLE clients DROP COLUMN termsUri; - -UPDATE dbMetadata SET value = '16' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-016-015.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-016-015.sql deleted file mode 100644 index 582dad6..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-016-015.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Adds new "termsUri" and "privacyUri" columns for third-party clients. - ---ALTER TABLE clients ADD COLUMN termsUri VARCHAR(256) NOT NULL AFTER redirectUri; ---ALTER TABLE clients ADD COLUMN privacyUri VARCHAR(256) NOT NULL AFTER termsUri; - ---UPDATE dbMetadata SET value = '15' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-016-017.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-016-017.sql deleted file mode 100644 index ce71f3e..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-016-017.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Add `lastUsedAt` column to the `refreshTokens` table. - -ALTER TABLE refreshTokens ADD COLUMN lastUsedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -ALGORITHM = INPLACE, LOCK = NONE; - -UPDATE dbMetadata SET value = '17' WHERE name = 'schema-patch-level'; \ No newline at end of file diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-017-016.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-017-016.sql deleted file mode 100644 index 802f46a..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-017-016.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Drop `lastUsedAt` column to the `refreshTokens` table. - --- ALTER TABLE refreshTokens DROP COLUMN lastUsedAt, --- ALGORITHM = INPLACE, LOCK = NONE; - --- UPDATE dbMetadata SET value = '16' WHERE name = 'schema-patch-level'; \ No newline at end of file diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-017-018.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-017-018.sql deleted file mode 100644 index 85b241d..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-017-018.sql +++ /dev/null @@ -1,11 +0,0 @@ --- Add `publicClient` column to the `clients` table. -ALTER TABLE clients ADD COLUMN publicClient BOOLEAN DEFAULT FALSE NOT NULL AFTER canGrant; -UPDATE clients SET publicClient=false; - --- Add `codeChallengeMethod` and `codeChallenge` column to the `codes` table. -ALTER TABLE codes -ADD COLUMN codeChallengeMethod VARCHAR(256) AFTER offline, -ADD COLUMN codeChallenge VARCHAR(256) AFTER codeChallengeMethod, -ALGORITHM = INPLACE, LOCK = NONE; - -UPDATE dbMetadata SET value = '18' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-018-017.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-018-017.sql deleted file mode 100644 index 197d452..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-018-017.sql +++ /dev/null @@ -1,14 +0,0 @@ --- Drop `publicClient` column from the `clients` table. - --- ALTER TABLE clients DROP COLUMN publicClient, --- ALGORITHM = INPLACE, LOCK = NONE; - --- Drop `codeChallengeMethod` and `codeChallenge` column from the `codes` table. - --- ALTER TABLE codes DROP COLUMN codeChallengeMethod, --- ALGORITHM = INPLACE, LOCK = NONE; - --- ALTER TABLE codes DROP COLUMN codeChallenge, --- ALGORITHM = INPLACE, LOCK = NONE; - --- UPDATE dbMetadata SET value = '17' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-018-019.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-018-019.sql deleted file mode 100644 index 58758c8..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-018-019.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE codes ADD COLUMN keysJwe MEDIUMTEXT, -ALGORITHM = INPLACE, LOCK = NONE; - -UPDATE dbMetadata SET value = '19' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-019-018.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-019-018.sql deleted file mode 100644 index e6e8b72..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-019-018.sql +++ /dev/null @@ -1,4 +0,0 @@ --- ALTER TABLE codes DROP COLUMN keysJwe, --- ALGORITHM = INPLACE, LOCK = NONE; - --- UPDATE dbMetadata SET value = '18' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-019-020.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-019-020.sql deleted file mode 100644 index b2010f0..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-019-020.sql +++ /dev/null @@ -1,10 +0,0 @@ -ALTER TABLE clients -ADD COLUMN allowedScopes VARCHAR(1024) AFTER trusted, -ALGORITHM = INPLACE, LOCK = NONE; - -CREATE TABLE scopes ( - scope VARCHAR(128) NOT NULL PRIMARY KEY, - hasScopedKeys BOOLEAN NOT NULL DEFAULT FALSE -) ENGINE=InnoDB; - -UPDATE dbMetadata SET value = '20' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-020-019.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-020-019.sql deleted file mode 100644 index 553097a..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-020-019.sql +++ /dev/null @@ -1,6 +0,0 @@ --- ALTER TABLE clients DROP COLUMN allowedScopes, --- ALGORITHM = INPLACE, LOCK = NONE; - --- DROP TABLE scopes; - --- UPDATE dbMetadata SET value = '19' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-020-021.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-020-021.sql deleted file mode 100644 index f170373..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-020-021.sql +++ /dev/null @@ -1,14 +0,0 @@ --- Add columns to stash 'amr' and 'aal' on the codes table. --- The 'amr' column will be a comma-separated string. --- It's tempting to use MySQL's SET datatype to save on storage space here: --- --- https://dev.mysql.com/doc/refman/5.7/en/set.html --- --- But codes are transient, so it doesn't seem worthwhile to --- trade the maintenance complexity for a little storage space. -ALTER TABLE codes -ADD COLUMN amr VARCHAR(128) AFTER authAt, -ADD COLUMN aal TINYINT AFTER amr, -ALGORITHM = INPLACE, LOCK = NONE; - -UPDATE dbMetadata SET value = '21' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-021-020.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-021-020.sql deleted file mode 100644 index e858871..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-021-020.sql +++ /dev/null @@ -1,6 +0,0 @@ --- ALTER TABLE codes --- DROP COLUMN amr, --- DROP COLUMN aal, --- ALGORITHM = INPLACE, LOCK = NONE; - --- UPDATE dbMetadata SET value = '20' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-021-022.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-021-022.sql deleted file mode 100644 index 49f76d3..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-021-022.sql +++ /dev/null @@ -1,11 +0,0 @@ --- Add column to stash the `profileChangedAt` value -ALTER TABLE codes ADD COLUMN profileChangedAt BIGINT DEFAULT NULL, -ALGORITHM = INPLACE, LOCK = NONE; - -ALTER TABLE tokens ADD COLUMN profileChangedAt BIGINT DEFAULT NULL, -ALGORITHM = INPLACE, LOCK = NONE; - -ALTER TABLE refreshTokens ADD COLUMN profileChangedAt BIGINT DEFAULT NULL, -ALGORITHM = INPLACE, LOCK = NONE; - -UPDATE dbMetadata SET value = '22' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-022-021.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-022-021.sql deleted file mode 100644 index b5ad5ba..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-022-021.sql +++ /dev/null @@ -1,13 +0,0 @@ --- ALTER TABLE tokens --- DROP COLUMN profileChangedAt, --- ALGORITHM = INPLACE, LOCK = NONE; - --- ALTER TABLE codes --- DROP COLUMN profileChangedAt, --- ALGORITHM = INPLACE, LOCK = NONE; - --- ALTER TABLE refreshTokens --- DROP COLUMN profileChangedAt, --- ALGORITHM = INPLACE, LOCK = NONE; - --- UPDATE dbMetadata SET value = '21`' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-022-023.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-022-023.sql deleted file mode 100644 index d513bbe..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-022-023.sql +++ /dev/null @@ -1,55 +0,0 @@ --- Drop foreign key constraints. They make DB migrations harder --- and aren't really providing us much value in practice. - --- The `clientDevelopers` table needs indexes on `developerId` a `clientId` --- for fast lookup. Prior to this patch, we were taking advantage of the --- index that is automatically created to enforce foreign key constraints, --- which the MySQL docs at [1] describe as: --- --- """ --- In the referencing table, there must be an index where the foreign key --- columns are listed as the first columns in the same order. Such an index --- is created on the referencing table automatically if it does not exist. --- This index might be silently dropped later, if you create another index --- that can be used to enforce the foreign key constraint. --- """ --- [1] https://dev.mysql.com/doc/refman/5.7/en/create-table-foreign-keys.html --- --- The "might" in there leaves some doubt about the exact circumstances under --- which we can depend on this index continuing to exist, so this migration --- explicitly creates the indexes we need. It's a two step process: --- --- 1) Explicitly create the indexes we need. This "might" cause the ones --- that were created automatically for the FK constraint to be dropped. --- --- 2) Drop the FK constraints, which might leave behind the auto-created --- indexes if they weren't dropped in (1) above. --- --- In my testing, the auto-created indexes are indeed dropped in favour --- of the explicit ones. If they aren't, then at least we wind up with --- duplicate indexes which can be cleaned up manually, which is much better --- than winding up with no indexes at all. --- - -ALTER TABLE clientDevelopers ADD INDEX idx_clientDevelopers_developerId(developerId), -ALGORITHM = INPLACE, LOCK = NONE; - -ALTER TABLE clientDevelopers ADD INDEX idx_clientDevelopers_clientId(clientId), -ALGORITHM = INPLACE, LOCK = NONE; - -ALTER TABLE clientDevelopers DROP FOREIGN KEY clientDevelopers_ibfk_1, -ALGORITHM = INPLACE, LOCK = NONE; - -ALTER TABLE clientDevelopers DROP FOREIGN KEY clientDevelopers_ibfk_2, -ALGORITHM = INPLACE, LOCK = NONE; - -ALTER TABLE refreshTokens DROP FOREIGN KEY refreshTokens_ibfk_1, -ALGORITHM = INPLACE, LOCK = NONE; - -ALTER TABLE codes DROP FOREIGN KEY codes_ibfk_1, -ALGORITHM = INPLACE, LOCK = NONE; - -ALTER TABLE tokens DROP FOREIGN KEY tokens_ibfk_1, -ALGORITHM = INPLACE, LOCK = NONE; - -UPDATE dbMetadata SET value = '23' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/patches/patch-023-022.sql b/fxa-oauth-server/lib/db/mysql/patches/patch-023-022.sql deleted file mode 100644 index ab1296e..0000000 --- a/fxa-oauth-server/lib/db/mysql/patches/patch-023-022.sql +++ /dev/null @@ -1,23 +0,0 @@ - --- ALTER TABLE clientDevelopers DROP INDEX idx_clientDevelopers_developerId, --- ALGORITHM = INPLACE, LOCK = NONE; - --- ALTER TABLE clientDevelopers DROP INDEX idx_clientDevelopers_clientId(clientId), --- ALGORITHM = INPLACE, LOCK = NONE; - --- ALTER TABLE clientDevelopers ADD FOREIGN KEY (developerId) REFERENCES developers(developerId) ON DELETE CASCADE, --- ALGORITHM = INPLACE, LOCK = NONE; - --- ALTER TABLE clientDevelopers ADD FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE, --- ALGORITHM = INPLACE, LOCK = NONE; - --- ALTER TABLE refreshTokens ADD FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE, --- ALGORITHM = INPLACE, LOCK = NONE; - --- ALTER TABLE codes ADD FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE, --- ALGORITHM = INPLACE, LOCK = NONE; - --- ALTER TABLE tokens ADD FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE, --- ALGORITHM = INPLACE, LOCK = NONE; - --- UPDATE dbMetadata SET value = '22' WHERE name = 'schema-patch-level'; diff --git a/fxa-oauth-server/lib/db/mysql/schema.sql b/fxa-oauth-server/lib/db/mysql/schema.sql deleted file mode 100644 index 904fa1e..0000000 --- a/fxa-oauth-server/lib/db/mysql/schema.sql +++ /dev/null @@ -1,84 +0,0 @@ --- --- This file represents the current db schema. --- It exists mainly for documentation purposes; any automated database --- modifications are controlled by the files in the ./patches/ directory. --- --- If you make a change here, you should also create a new database patch --- file and increment the level in ./patch.js to reflect the change. --- - -CREATE TABLE IF NOT EXISTS clients ( - id BINARY(8) PRIMARY KEY, - hashedSecret BINARY(32), - hashedSecretPrevious BINARY(32), - name VARCHAR(256) NOT NULL, - imageUri VARCHAR(256) NOT NULL, - redirectUri VARCHAR(256) NOT NULL, - canGrant BOOLEAN DEFAULT FALSE, - createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - trusted BOOLEAN DEFAULT FALSE, - allowedScopes VARCHAR(1024) -) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci; - -CREATE TABLE IF NOT EXISTS codes ( - code BINARY(32) PRIMARY KEY, - clientId BINARY(8) NOT NULL, - INDEX codes_client_id(clientId), - userId BINARY(16) NOT NULL, - INDEX codes_user_id(userId), - email VARCHAR(256) NOT NULL, - scope VARCHAR(256) NOT NULL, - createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - authAt BIGINT DEFAULT 0, - offline BOOLEAN DEFAULT FALSE, - profileChangedAt BIGINT DEFAULT NULL -) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci; - -CREATE TABLE IF NOT EXISTS tokens ( - token BINARY(32) PRIMARY KEY, - clientId BINARY(8) NOT NULL, - INDEX tokens_client_id(clientId), - userId BINARY(16) NOT NULL, - INDEX tokens_user_id(userId), - email VARCHAR(256) NOT NULL, - type VARCHAR(16) NOT NULL, - scope VARCHAR(256) NOT NULL, - createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - expiresAt TIMESTAMP NOT NULL, - profileChangedAt BIGINT DEFAULT NULL, - INDEX idx_expiresAt(expiresAt) -) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci; - -CREATE TABLE IF NOT EXISTS developers ( - developerId BINARY(16) NOT NULL, - email VARCHAR(255) NOT NULL, - createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE(email) -) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci; - -CREATE TABLE IF NOT EXISTS clientDevelopers ( - rowId BINARY(8) PRIMARY KEY, - developerId BINARY(16) NOT NULL, - clientId BINARY(8) NOT NULL, - createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - INDEX idx_clientDevelopers_developerId(developerId), - INDEX idx_clientDevelopers_clientId(clientId) -) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci; - -CREATE TABLE IF NOT EXISTS refreshTokens ( - token BINARY(32) PRIMARY KEY, - clientId BINARY(8) NOT NULL, - INDEX tokens_client_id(clientId), - userId BINARY(16) NOT NULL, - INDEX tokens_user_id(userId), - email VARCHAR(256) NOT NULL, - scope VARCHAR(256) NOT NULL, - createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - lastUsedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - profileChangedAt BIGINT DEFAULT NULL -) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci; - -CREATE TABLE scopes ( - scope VARCHAR(128) NOT NULL PRIMARY KEY, - hasScopedKeys BOOLEAN NOT NULL DEFAULT FALSE -) ENGINE=InnoDB; diff --git a/fxa-oauth-server/lib/encrypt.js b/fxa-oauth-server/lib/encrypt.js deleted file mode 100644 index 6fdce5b..0000000 --- a/fxa-oauth-server/lib/encrypt.js +++ /dev/null @@ -1,15 +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/. */ - -const crypto = require('crypto'); - -const buf = require('buf').hex; - -const config = require('./config'); - -exports.hash = function hash(value) { - var sha = crypto.createHash(config.get('encrypt.hashAlg')); - sha.update(buf(value)); - return sha.digest(); -}; diff --git a/fxa-oauth-server/lib/env.js b/fxa-oauth-server/lib/env.js deleted file mode 100644 index f46c879..0000000 --- a/fxa-oauth-server/lib/env.js +++ /dev/null @@ -1,10 +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/. */ - -const config = require('./config'); - -exports.isProdLike = function isProdLike() { - var env = config.get('env'); - return env === 'prod' || env === 'stage'; -}; diff --git a/fxa-oauth-server/lib/error.js b/fxa-oauth-server/lib/error.js deleted file mode 100644 index bee5434..0000000 --- a/fxa-oauth-server/lib/error.js +++ /dev/null @@ -1,288 +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/. */ - -const util = require('util'); - -const DEFAULTS = { - code: 500, - error: 'Internal Server Error', - errno: 999, - info: 'https://github.com/mozilla/fxa-oauth-server/blob/master/docs/api.md#errors', - message: 'Unspecified error' -}; - -function merge(target, other) { - var keys = Object.keys(other || {}); - for (var i = 0; i < keys.length; i++) { - target[keys[i]] = other[keys[i]]; - } -} - -function AppError(options, extra, headers) { - this.message = options.message || DEFAULTS.message; - this.isBoom = true; - if (options.stack) { - this.stack = options.stack; - } else { - Error.captureStackTrace(this, AppError); - } - this.errno = options.errno || DEFAULTS.errno; - this.output = { - statusCode: options.code || DEFAULTS.code, - payload: { - code: options.code || DEFAULTS.code, - errno: this.errno, - error: options.error || DEFAULTS.error, - message: this.message, - info: options.info || DEFAULTS.info - }, - headers: headers || {} - }; - merge(this.output.payload, extra); -} -util.inherits(AppError, Error); - -AppError.prototype.toString = function () { - return 'Error: ' + this.message; -}; - -AppError.prototype.header = function (name, value) { - this.output.headers[name] = value; -}; - -AppError.translate = function translate(response) { - if (response instanceof AppError) { - return response; - } - - var error; - var payload = response.output.payload; - if (payload.validation) { - error = AppError.invalidRequestParameter(payload.validation); - } else if (payload.statusCode === 415) { - error = AppError.invalidContentType(); - } else { - error = new AppError({ - message: payload.message, - code: payload.statusCode, - error: payload.error, - errno: payload.errno, - stack: response.stack - }); - } - - return error; -}; - -AppError.unknownClient = function unknownClient(clientId) { - return new AppError({ - code: 400, - error: 'Bad Request', - errno: 101, - message: 'Unknown client' - }, { - clientId: clientId - }); -}; - -AppError.incorrectSecret = function incorrectSecret(clientId) { - return new AppError({ - code: 400, - error: 'Bad Request', - errno: 102, - message: 'Incorrect secret' - }, { - clientId: clientId - }); -}; - -AppError.incorrectRedirect = function incorrectRedirect(uri) { - return new AppError({ - code: 400, - error: 'Bad Request', - errno: 103, - message: 'Incorrect redirect_uri' - }, { - redirectUri: uri - }); -}; - -AppError.invalidAssertion = function invalidAssertion() { - return new AppError({ - code: 401, - error: 'Bad Request', - errno: 104, - message: 'Invalid assertion' - }); -}; - -AppError.unknownCode = function unknownCode(code) { - return new AppError({ - code: 400, - error: 'Bad Request', - errno: 105, - message: 'Unknown code' - }, { - requestCode: code - }); -}; - -AppError.mismatchCode = function mismatchCode(code, clientId) { - return new AppError({ - code: 400, - error: 'Bad Request', - errno: 106, - message: 'Incorrect code' - }, { - requestCode: code, - client: clientId - }); -}; - -AppError.expiredCode = function expiredCode(code, expiredAt) { - return new AppError({ - code: 400, - error: 'Bad Request', - errno: 107, - message: 'Expired code' - }, { - requestCode: code, - expiredAt: expiredAt - }); -}; - -AppError.invalidToken = function invalidToken() { - return new AppError({ - code: 400, - error: 'Bad Request', - errno: 108, - message: 'Invalid token' - }); -}; - -AppError.invalidRequestParameter = function invalidRequestParameter(val) { - return new AppError({ - code: 400, - error: 'Bad Request', - errno: 109, - message: 'Invalid request parameter' - }, { - validation: val - }); -}; - -AppError.invalidResponseType = function invalidResponseType() { - return new AppError({ - code: 400, - error: 'Bad Request', - errno: 110, - message: 'Invalid response_type' - }); -}; - -AppError.unauthorized = function unauthorized(reason) { - return new AppError({ - code: 401, - error: 'Unauthorized', - errno: 111, - message: 'Unauthorized for route' - }, { - detail: reason - }); -}; - -AppError.forbidden = function forbidden() { - return new AppError({ - code: 403, - error: 'Forbidden', - errno: 112, - message: 'Forbidden' - }); -}; - -AppError.invalidContentType = function invalidContentType() { - return new AppError({ - code: 415, - error: 'Unsupported Media Type', - errno: 113, - message: 'Content-Type must be either application/json or ' + - 'application/x-www-form-urlencoded' - }); -}; - -AppError.invalidScopes = function invalidScopes(scopes) { - return new AppError({ - code: 400, - error: 'Invalid scopes', - errno: 114, - message: 'Requested scopes are not allowed' - }, { - invalidScopes: scopes - }); -}; - -AppError.expiredToken = function expiredToken(expiredAt) { - return new AppError({ - code: 400, - error: 'Bad Request', - errno: 115, - message: 'Expired token' - }, { - expiredAt: expiredAt - }); -}; - -AppError.notPublicClient = function unknownClient(clientId) { - return new AppError({ - code: 400, - error: 'Bad Request', - errno: 116, - message: 'Not a public client' - }, { - clientId: clientId - }); -}; - - -AppError.mismatchCodeChallenge = function mismatchCodeChallenge(pkceHashValue) { - return new AppError({ - code: 400, - error: 'Bad Request', - errno: 117, - message: 'Incorrect code_challenge' - }, { - requestCodeChallenge: pkceHashValue - }); -}; - -AppError.missingPkceParameters = function missingPkceParameters() { - return new AppError({ - code: 400, - error: 'PKCE parameters missing', - errno: 118, - message: 'Public clients require PKCE OAuth parameters' - }); -}; - -AppError.staleAuthAt = function staleAuthAt(authAt) { - return new AppError({ - code: 401, - error: 'Bad Request', - errno: 119, - message: 'Stale authentication timestamp' - }, { - authAt: authAt - }); -}; - -AppError.mismatchAcr = function mismatchAcr(foundValue) { - return new AppError({ - code: 400, - error: 'Bad Request', - errno: 120, - message: 'Mismatch acr value' - }, {foundValue}); -}; - -module.exports = AppError; diff --git a/fxa-oauth-server/lib/events.js b/fxa-oauth-server/lib/events.js deleted file mode 100644 index 21e2a47..0000000 --- a/fxa-oauth-server/lib/events.js +++ /dev/null @@ -1,64 +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/. */ - -const config = require('./config').getProperties(); -const db = require('./db'); -const P = require('./promise'); -const env = require('./env'); -const logger = require('./logging')('events'); -const Sink = require('fxa-notifier-aws').Sink; -const HEX_STRING = require('./validators').HEX_STRING; - -let fxaEvents; - -if (! config.events.region || ! config.events.queueUrl) { - fxaEvents = { - start: function start() { - if (env.isProdLike()) { - throw new Error('config.events must be included in prod'); - } else { - logger.warn('accountEvent.unconfigured'); - } - } - }; -} else { - fxaEvents = new Sink(config.events.region, config.events.queueUrl); - - fxaEvents.on('data', (message) => { - const messageEvent = message.event; - const uid = message.uid; - logger.verbose('data', message); - logger.info(message.event, {uid: uid}); - - if (! HEX_STRING.test(uid)) { - message.del(); - return logger.warn('badDelete', {userId: uid}); - } - - return P.resolve() - .then(() => { - switch (messageEvent) { - case 'delete': - return db.removeUser(uid); - case 'reset': - case 'passwordChange': - return db.removePublicAndCanGrantTokens(uid); - default: - return; - } - }) - .done(() => { - message.del(); - }, - (err) => { - logger.error(message.event, err); - }); - }); - - fxaEvents.on('error', (err) => logger.error('accountEvent', err)); - fxaEvents.start = fxaEvents.fetch; -} - - -module.exports = fxaEvents; diff --git a/fxa-oauth-server/lib/logging/index.js b/fxa-oauth-server/lib/logging/index.js deleted file mode 100644 index 9d92674..0000000 --- a/fxa-oauth-server/lib/logging/index.js +++ /dev/null @@ -1,15 +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/. */ - -const config = require('../config').get('logging'); - -const mozlog = require('mozlog')(config); - -var root = mozlog(config.app); -if (root.isEnabledFor('debug')) { - root.warn('\t*** CAREFUL! Louder logs (less than INFO)' + - ' may include SECRETS! ***'); -} - -module.exports = mozlog; diff --git a/fxa-oauth-server/lib/logging/summary.js b/fxa-oauth-server/lib/logging/summary.js deleted file mode 100644 index d544331..0000000 --- a/fxa-oauth-server/lib/logging/summary.js +++ /dev/null @@ -1,53 +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/. */ - -const logger = require('./')('summary'); - -function parsePayload(payload) { - var payloadKeys = ['INVALID_PAYLOAD_OBJECT']; - try { - // given payload object might not be a valid object - // See issue #410 - payloadKeys = Object.keys(payload); - } catch (e) { - // failed to parse payload keys. - } - return payloadKeys; -} - -module.exports = function summary(request, response) { - /*eslint complexity: [2, 11] */ - if (request.method === 'options') { - return; - } - var payload = request.payload || {}; - var query = request.query || {}; - var params = request.params || {}; - - var auth = request.auth && request.auth.credentials && { - user: request.auth.credentials.user, - scope: request.auth.credentials.scope - }; - - - var line = { - code: response.isBoom ? response.output.statusCode : response.statusCode, - errno: response.errno || 0, - method: request.method, - path: request.path, - agent: request.headers['user-agent'], - t: Date.now() - request.info.received, - client_id: payload.client_id || query.client_id || params.client_id, - auth: auth, - payload: parsePayload(payload), - remoteAddressChain: request.app.remoteAddressChain - }; - - if (line.code >= 500) { - line.stack = response.stack; - logger.error('summary', line); - } else { - logger.info('summary', line); - } -}; diff --git a/fxa-oauth-server/lib/promise.js b/fxa-oauth-server/lib/promise.js deleted file mode 100644 index 69aad5b..0000000 --- a/fxa-oauth-server/lib/promise.js +++ /dev/null @@ -1,6 +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/. */ - -module.exports = require('bluebird'); -module.exports.onPossiblyUnhandledRejection(); diff --git a/fxa-oauth-server/lib/routes/authorization.js b/fxa-oauth-server/lib/routes/authorization.js deleted file mode 100644 index 5c5ea75..0000000 --- a/fxa-oauth-server/lib/routes/authorization.js +++ /dev/null @@ -1,321 +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/. */ - -const buf = require('buf').hex; -const hex = require('buf').to.hex; -const Joi = require('joi'); -const URI = require('urijs'); - -const AppError = require('../error'); -const config = require('../config'); -const db = require('../db'); -const logger = require('../logging')('routes.authorization'); -const P = require('../promise'); -const ScopeSet = require('fxa-shared').oauth.scopes; -const validators = require('../validators'); -const verify = require('../browserid'); - -const CODE = 'code'; -const TOKEN = 'token'; - -const ACCESS_TYPE_ONLINE = 'online'; -const ACCESS_TYPE_OFFLINE = 'offline'; - -const ACR_VALUE_AAL2 = 'AAL2'; - -const PKCE_SHA256_CHALLENGE_METHOD = 'S256'; // This server only supports S256 PKCE, no 'plain' -const PKCE_CODE_CHALLENGE_LENGTH = 43; - -const MAX_TTL_S = config.get('expiration.accessToken') / 1000; - -const UNTRUSTED_CLIENT_ALLOWED_SCOPES = ScopeSet.fromArray([ - 'openid', - 'profile:uid', - 'profile:email', - 'profile:display_name' -]); - -const allowHttpRedirects = config.get('allowHttpRedirects'); - -var ALLOWED_SCHEMES = [ - 'https' -]; - -if (allowHttpRedirects === true) { - // http scheme used when developing OAuth clients - ALLOWED_SCHEMES.push('http'); -} - -function isLocalHost(url) { - var host = new URI(url).hostname(); - return host === 'localhost' || host === '127.0.0.1'; -} - -function generateCode(claims, client, scope, req) { - return db.generateCode({ - clientId: client.id, - userId: buf(claims.uid), - email: claims['fxa-verifiedEmail'], - scope: scope, - authAt: claims['fxa-lastAuthAt'], - amr: claims['fxa-amr'], - aal: claims['fxa-aal'], - offline: req.payload.access_type === ACCESS_TYPE_OFFLINE, - codeChallengeMethod: req.payload.code_challenge_method, - codeChallenge: req.payload.code_challenge, - keysJwe: req.payload.keys_jwe, - profileChangedAt: claims['fxa-profileChangedAt'] - }).then(function(code) { - logger.debug('redirecting', { uri: req.payload.redirect_uri }); - - code = hex(code); - const redirect = URI(req.payload.redirect_uri) - .addQuery({ state: req.payload.state, code }); - - const out = { - code, - state: req.payload.state, - redirect: String(redirect) - }; - logger.info('generateCode', { - request: { - client_id: req.payload.client_id, - redirect_uri: req.payload.redirect_uri, - scope: req.payload.scope, - state: req.payload.state, - response_type: req.payload.response_type - }, - response: out - }); - return out; - }); -} - -function generateGrant(claims, client, scope, req) { - return db.generateAccessToken({ - clientId: client.id, - userId: buf(claims.uid), - email: claims['fxa-verifiedEmail'], - scope: scope, - ttl: req.payload.ttl, - profileChangedAt: claims['fxa-profileChangedAt'] - }).then(function(token) { - return { - access_token: hex(token.token), - token_type: 'bearer', - expires_in: Math.floor((token.expiresAt - Date.now()) / 1000), - scope: scope.toString(), - auth_at: claims['fxa-lastAuthAt'] - }; - }); -} - -// Check that PKCE is provided if and only if appropriate. -function checkPKCEParams(req, client) { - if (req.payload.response_type === TOKEN) { - // Direct token grant can't use PKCE. - if (req.payload.code_challenge_method) { - throw new AppError.invalidRequestParameter('code_challenge_method'); - } - if (req.payload.code_challenge) { - throw new AppError.invalidRequestParameter('code_challenge'); - } - } else if (client.publicClient) { - // Public clients *must* use PKCE. - if (! req.payload.code_challenge_method || ! req.payload.code_challenge) { - logger.info('client.missingPkceParameters'); - throw AppError.missingPkceParameters(); - } - } else { - // non-Public Clients can't use PKCE. - if (req.payload.code_challenge_method || req.payload.code_challenge) { - logger.info('client.notPublicClient'); - throw AppError.notPublicClient({ id: req.payload.client_id }); - } - } -} - -module.exports = { - validate: { - payload: { - client_id: validators.clientId, - assertion: validators.assertion - .required(), - redirect_uri: Joi.string() - .max(256) - // uri validation ref: https://github.com/hapijs/joi/blob/master/API.md#stringurioptions - .uri({ - scheme: ALLOWED_SCHEMES - }), - scope: validators.scope, - response_type: Joi.string() - .valid(CODE, TOKEN) - .default(CODE), - state: Joi.string() - .max(256) - .when('response_type', { - is: TOKEN, - then: Joi.optional(), - otherwise: Joi.required() - }), - ttl: Joi.number() - .positive() - .max(MAX_TTL_S) - .default(MAX_TTL_S) - .when('response_type', { - is: TOKEN, - then: Joi.optional(), - otherwise: Joi.forbidden() - }), - access_type: Joi.string() - .valid(ACCESS_TYPE_OFFLINE, ACCESS_TYPE_ONLINE) - .default(ACCESS_TYPE_ONLINE) - .optional(), - code_challenge_method: Joi.string() - .valid(PKCE_SHA256_CHALLENGE_METHOD) - .when('response_type', { - is: CODE, - then: Joi.optional(), - otherwise: Joi.forbidden() - }), - code_challenge: Joi.string() - .length(PKCE_CODE_CHALLENGE_LENGTH) - .when('response_type', { - is: CODE, - then: Joi.optional(), - otherwise: Joi.forbidden() - }), - keys_jwe: validators.jwe - .when('response_type', { - is: CODE, - then: Joi.optional(), - otherwise: Joi.forbidden() - }), - acr_values: Joi.string().max(256).optional().allow(null) - } - }, - response: { - schema: Joi.object().keys({ - redirect: Joi.string(), - code: Joi.string(), - state: Joi.string(), - access_token: validators.token, - token_type: Joi.string().valid('bearer'), - scope: Joi.string().allow(''), - auth_at: Joi.number(), - expires_in: Joi.number() - }).with('access_token', [ - 'token_type', - 'scope', - 'auth_at', - 'expires_in' - ]).with('code', [ - 'state', - 'redirect', - ]).without('code', [ - 'access_token' - ]) - }, - handler: async function authorizationEndpoint(req) { - /*eslint complexity: [2, 13] */ - logger.debug('response_type', req.payload.response_type); - var start = Date.now(); - var wantsGrant = req.payload.response_type === TOKEN; - var exitEarly = false; - var scope = ScopeSet.fromString(req.payload.scope || ''); - return P.all([ - verify(req.payload.assertion).then(function(claims) { - logger.info('time.browserid_verify', { ms: Date.now() - start }); - if (! claims) { - exitEarly = true; - throw AppError.invalidAssertion(); - } - - // Check to see if the acr value requested by oauth matches what is expected - const acrValues = req.payload.acr_values; - if (acrValues) { - const acrTokens = acrValues.split('\s+'); - if (acrTokens.includes(ACR_VALUE_AAL2) && ! (claims['fxa-aal'] >= 2)) { - throw AppError.mismatchAcr(claims['fxa-aal']); - } - } - - // Any request for a key-bearing scope should be using a verified token. - // Double-check that here as a defense-in-depth measure. - if (! claims['fxa-tokenVerified']) { - return P.each(scope.getScopeValues(), scope => { - // Don't bother hitting the DB if other checks have failed. - if (exitEarly) { - return; - } - // We know only URL-format scopes can have keys, - // so avoid trips to the DB for common scopes like 'profile'. - if (scope.startsWith('https://')) { - return db.getScope(scope).then(s => { - if (s && s.hasScopedKeys) { - exitEarly = true; - throw AppError.invalidAssertion(); - } - }); - } - }).then(() => { - return claims; - }); - } - return claims; - }), - db.getClient(Buffer.from(req.payload.client_id, 'hex')).then(function(client) { - logger.info('time.db_get_client', { ms: Date.now() - start }); - if (exitEarly) { - // assertion was invalid, we can just stop here - return; - } - if (! client) { - logger.debug('notFound', { id: req.payload.client_id }); - throw AppError.unknownClient(req.payload.client_id); - } else if (! client.trusted) { - var invalidScopes = scope.difference(UNTRUSTED_CLIENT_ALLOWED_SCOPES); - if (! invalidScopes.isEmpty()) { - throw AppError.invalidScopes(invalidScopes.getScopeValues()); - } - } - - var uri = req.payload.redirect_uri || client.redirectUri; - - if (uri !== client.redirectUri) { - logger.debug('redirect.mismatch', { - param: uri, - registered: client.redirectUri - }); - - if (config.get('localRedirects') && isLocalHost(uri)) { - logger.debug('redirect.local', { uri: uri }); - } else { - throw AppError.incorrectRedirect(uri); - } - - } - - if (wantsGrant && ! client.canGrant) { - logger.warn('implicitGrant.notAllowed', { - id: req.payload.client_id - }); - throw AppError.invalidResponseType(); - } - - req.payload.redirect_uri = uri; - - checkPKCEParams(req, client); - - return client; - }).catch(err => { - exitEarly = true; - throw err; - }), - scope, - req - ]) - .spread(wantsGrant ? generateGrant : generateCode); - } -}; diff --git a/fxa-oauth-server/lib/routes/client-tokens/delete.js b/fxa-oauth-server/lib/routes/client-tokens/delete.js deleted file mode 100644 index 85bc635..0000000 --- a/fxa-oauth-server/lib/routes/client-tokens/delete.js +++ /dev/null @@ -1,20 +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/. */ - -const db = require('../../db'); -const SCOPE_CLIENT_WRITE = require('../../auth_bearer').SCOPE_CLIENT_WRITE; - -module.exports = { - auth: { - strategy: 'authBearer', - scope: SCOPE_CLIENT_WRITE.getImplicantValues() - }, - handler: async function activeServices(req) { - var clientId = req.params.client_id; - return db.deleteClientAuthorization(clientId, req.auth.credentials.user) - .then(function() { - return {}; - }); - } -}; diff --git a/fxa-oauth-server/lib/routes/client-tokens/list.js b/fxa-oauth-server/lib/routes/client-tokens/list.js deleted file mode 100644 index 87bd736..0000000 --- a/fxa-oauth-server/lib/routes/client-tokens/list.js +++ /dev/null @@ -1,40 +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/. */ - -const hex = require('buf').to.hex; -const config = require('../../config'); -const db = require('../../db'); -const SCOPE_CLIENT_WRITE = require('../../auth_bearer').SCOPE_CLIENT_WRITE; -const localizeTimestamp = require('fxa-shared').l10n.localizeTimestamp({ - supportedLanguages: config.get('i18n.supportedLanguages'), - defaultLanguage: config.get('i18n.defaultLanguage') -}); - -function serialize(client, acceptLanguage) { - var lastAccessTime = client.lastAccessTime.getTime(); - var lastAccessTimeFormatted = localizeTimestamp.format(lastAccessTime, acceptLanguage); - - return { - name: client.name, - id: hex(client.id), - lastAccessTime: lastAccessTime, - lastAccessTimeFormatted: lastAccessTimeFormatted, - scope: client.scope - }; -} - -module.exports = { - auth: { - strategy: 'authBearer', - scope: SCOPE_CLIENT_WRITE.getImplicantValues() - }, - handler: async function activeServices(req) { - return db.getActiveClientsByUid(req.auth.credentials.user) - .then(function(clients) { - return clients.map(function(client) { - return serialize(client, req.headers['accept-language']); - }); - }); - } -}; diff --git a/fxa-oauth-server/lib/routes/client/delete.js b/fxa-oauth-server/lib/routes/client/delete.js deleted file mode 100644 index 376720e..0000000 --- a/fxa-oauth-server/lib/routes/client/delete.js +++ /dev/null @@ -1,36 +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/. */ - -const auth = require('../../auth'); -const db = require('../../db'); -const validators = require('../../validators'); -const AppError = require('../../error'); - -module.exports = { - auth: { - strategy: auth.AUTH_STRATEGY, - scope: auth.SCOPE_CLIENT_MANAGEMENT.getImplicantValues() - }, - validate: { - params: { - client_id: validators.clientId - } - }, - handler: async function clientDeleteEndpoint(req, h) { - var email = req.auth.credentials.email; - var clientId = req.params.client_id; - - return db.developerOwnsClient(email, clientId) - .then( - function () { - return db.removeClient(clientId).then(function() { - return h.response({}).code(204); - }); - }, - function () { - throw new AppError.unauthorized('Illegal Developer'); - } - ); - } -}; diff --git a/fxa-oauth-server/lib/routes/client/get.js b/fxa-oauth-server/lib/routes/client/get.js deleted file mode 100644 index 1f32ae5..0000000 --- a/fxa-oauth-server/lib/routes/client/get.js +++ /dev/null @@ -1,58 +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/. */ - -const hex = require('buf').to.hex; -const Joi = require('joi'); - -const AppError = require('../../error'); -const db = require('../../db'); -const logger = require('../../logging')('routes.client.get'); -const validators = require('../../validators'); - -module.exports = { - validate: { - params: { - client_id: validators.clientId - } - }, - response: { - schema: { - id: validators.clientId, - name: Joi.string().required(), - trusted: Joi.boolean().required(), - image_uri: Joi.any(), - redirect_uri: Joi.string().required().allow('') - } - }, - handler: async function requestInfoEndpoint(req) { - var params = req.params; - - function makeReq() { - return new Promise((resolve) => { - return db.getClient(Buffer.from(params.client_id, 'hex')).then(function (client) { - if (! client) { - logger.debug('notFound', {id: params.client_id}); - throw AppError.unknownClient(params.client_id); - } - return client; - }).done(function (client) { - resolve({ - id: hex(client.id), - name: client.name, - trusted: client.trusted, - image_uri: client.imageUri, - redirect_uri: client.redirectUri - }); - }); - }); - } - - return makeReq().then((resp) => { - return resp; - }) - .catch((err) => { - throw err; - }); - } -}; diff --git a/fxa-oauth-server/lib/routes/client/list.js b/fxa-oauth-server/lib/routes/client/list.js deleted file mode 100644 index 85d4253..0000000 --- a/fxa-oauth-server/lib/routes/client/list.js +++ /dev/null @@ -1,51 +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/. */ - -const Joi = require('joi'); -const hex = require('buf').to.hex; - -const auth = require('../../auth'); -const db = require('../../db'); -const validators = require('../../validators'); - -function serialize(client) { - return { - id: hex(client.id), - name: client.name, - image_uri: client.imageUri, - redirect_uri: client.redirectUri, - can_grant: client.canGrant, - trusted: client.trusted - }; -} - -module.exports = { - auth: { - strategy: auth.AUTH_STRATEGY, - scope: auth.SCOPE_CLIENT_MANAGEMENT.getImplicantValues() - }, - response: { - schema: { - clients: Joi.array().items( - Joi.object().keys({ - id: validators.clientId, - name: Joi.string().required(), - image_uri: Joi.string().allow(''), - redirect_uri: Joi.string().allow('').required(), - can_grant: Joi.boolean().required(), - trusted: Joi.boolean().required() - }) - ) - } - }, - handler: async function listEndpoint(req) { - const developerEmail = req.auth.credentials.email; - - return db.getClients(developerEmail).then(function(clients) { - return { - clients: clients.map(serialize) - }; - }); - } -}; diff --git a/fxa-oauth-server/lib/routes/client/register.js b/fxa-oauth-server/lib/routes/client/register.js deleted file mode 100644 index 0dc0b60..0000000 --- a/fxa-oauth-server/lib/routes/client/register.js +++ /dev/null @@ -1,82 +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/. */ - -const Joi = require('joi'); - -const auth = require('../../auth'); -const db = require('../../db'); -const encrypt = require('../../encrypt'); -const hex = require('buf').to.hex; -const unique = require('../../unique'); -const validators = require('../../validators'); -const AppError = require('../../error'); - -module.exports = { - auth: { - strategy: auth.AUTH_STRATEGY, - scope: auth.SCOPE_CLIENT_MANAGEMENT.getImplicantValues() - }, - validate: { - payload: { - name: Joi.string().max(256).required(), - image_uri: Joi.string().max(256).allow(''), - redirect_uri: Joi.string().max(256).required(), - can_grant: Joi.boolean(), - trusted: Joi.boolean() - } - }, - response: { - schema: { - id: validators.clientId, - secret: validators.clientSecret, - name: Joi.string().required(), - image_uri: Joi.string().allow(''), - redirect_uri: Joi.string().required(), - can_grant: Joi.boolean().required(), - trusted: Joi.boolean().required() - } - }, - handler: async function registerEndpoint(req, h) { - var payload = req.payload; - var secret = unique.secret(); - var client = { - id: unique.id(), - hashedSecret: encrypt.hash(secret), - name: payload.name, - redirectUri: payload.redirect_uri, - imageUri: payload.image_uri || '', - canGrant: !! payload.can_grant, - trusted: !! payload.trusted - }; - var developerEmail = req.auth.credentials.email; - var developerId = null; - - return db.getDeveloper(developerEmail) - .then(function (developer) { - - // must be a developer to register clients - if (! developer) { - throw AppError.unauthorized('Illegal Developer'); - } - - developerId = developer.developerId; - - return db.registerClient(client); - }) - .then(function() { - return db.registerClientDeveloper(developerId, hex(client.id)); - }) - .then(function() { - return h.response({ - id: hex(client.id), - secret: hex(secret), - name: client.name, - redirect_uri: client.redirectUri, - image_uri: client.imageUri, - can_grant: client.canGrant, - trusted: client.trusted - }).code(201); - }); - } -}; diff --git a/fxa-oauth-server/lib/routes/client/update.js b/fxa-oauth-server/lib/routes/client/update.js deleted file mode 100644 index 9c4ea2c..0000000 --- a/fxa-oauth-server/lib/routes/client/update.js +++ /dev/null @@ -1,52 +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/. */ - -const buf = require('buf').hex; -const Joi = require('joi'); - -const auth = require('../../auth'); -const db = require('../../db'); -const validators = require('../../validators'); -const AppError = require('../../error'); - -module.exports = { - auth: { - strategy: auth.AUTH_STRATEGY, - scope: auth.SCOPE_CLIENT_MANAGEMENT.getImplicantValues() - }, - validate: { - params: { - client_id: validators.clientId - }, - payload: { - name: Joi.string().max(256), - image_uri: Joi.string().max(256), - redirect_uri: Joi.string().max(256), - can_grant: Joi.boolean() - } - }, - handler: async function updateClientEndpoint(req, reply) { - const clientId = req.params.client_id; - const payload = req.payload; - const email = req.auth.credentials.email; - - return db.developerOwnsClient(email, clientId) - .then( - function () { - return db.updateClient({ - id: buf(clientId), - name: payload.name, - redirectUri: payload.redirect_uri, - imageUri: payload.image_uri, - canGrant: payload.can_grant - }).then(function() { - return {}; - }); - }, - function () { - throw AppError.unauthorized('Illegal Developer'); - } - ); - } -}; diff --git a/fxa-oauth-server/lib/routes/config.js b/fxa-oauth-server/lib/routes/config.js deleted file mode 100644 index 80c9110..0000000 --- a/fxa-oauth-server/lib/routes/config.js +++ /dev/null @@ -1,19 +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/. */ - -const config = require('../config'); - -const CONFIG = { - browserid: { - issuer: config.get('browserid.issuer'), - verificationUrl: config.get('browserid.verificationUrl') - }, - contentUrl: config.get('contentUrl') -}; - -module.exports = { - handler: async function configRoute() { - return CONFIG; - } -}; diff --git a/fxa-oauth-server/lib/routes/destroy.js b/fxa-oauth-server/lib/routes/destroy.js deleted file mode 100644 index 09a328d..0000000 --- a/fxa-oauth-server/lib/routes/destroy.js +++ /dev/null @@ -1,45 +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/. */ - -const Joi = require('joi'); - -const AppError = require('../error'); -const db = require('../db'); -const encrypt = require('../encrypt'); -const validators = require('../validators'); - -/*jshint camelcase: false*/ - -module.exports = { - validate: { - payload: Joi.object().keys({ - client_secret: Joi.string().allow(''), - access_token: validators.token, - refresh_token: validators.token - }).rename('token', 'access_token').xor('access_token', 'refresh_token') - }, - handler: async function destroyToken(req) { - var token; - var getToken; - var removeToken; - if (req.payload.access_token) { - getToken = 'getAccessToken'; - removeToken = 'removeAccessToken'; - token = encrypt.hash(req.payload.access_token); - } else { - getToken = 'getRefreshToken'; - removeToken = 'removeRefreshToken'; - token = encrypt.hash(req.payload.refresh_token); - } - - return db[getToken](token).then(function(tok) { - if (! tok) { - throw AppError.invalidToken(); - } - return db[removeToken](token); - }).then(function() { - return {}; - }); - } -}; diff --git a/fxa-oauth-server/lib/routes/developer/activate.js b/fxa-oauth-server/lib/routes/developer/activate.js deleted file mode 100644 index e00485c..0000000 --- a/fxa-oauth-server/lib/routes/developer/activate.js +++ /dev/null @@ -1,35 +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/. */ - -const auth = require('../../auth'); -const db = require('../../db'); -const hex = require('buf').to.hex; - -function developerResponse(developer) { - return { - developerId: hex(developer.developerId), - email: developer.email, - createdAt: developer.createdAt - }; -} - -module.exports = { - auth: { - strategy: auth.AUTH_STRATEGY, - scope: auth.SCOPE_CLIENT_MANAGEMENT.getImplicantValues() - }, - handler: async function activateRegistration(req) { - const email = req.auth.credentials.email; - - return db.getDeveloper(email) - .then(function(developer) { - if (developer) { - return developer; - } else { - return db.activateDeveloper(email); - } - }) - .then(developerResponse); - } -}; diff --git a/fxa-oauth-server/lib/routes/heartbeat.js b/fxa-oauth-server/lib/routes/heartbeat.js deleted file mode 100644 index c6f95af..0000000 --- a/fxa-oauth-server/lib/routes/heartbeat.js +++ /dev/null @@ -1,11 +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/. */ - -const db = require('../db'); - -module.exports = { - handler: async function heartbeat() { - return db.ping(); - } -}; diff --git a/fxa-oauth-server/lib/routes/jwks.js b/fxa-oauth-server/lib/routes/jwks.js deleted file mode 100644 index c822caa..0000000 --- a/fxa-oauth-server/lib/routes/jwks.js +++ /dev/null @@ -1,40 +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/. */ - -const config = require('../config'); - -const KEYS = (function() { - var priv = config.get('openid.key'); - - function pub(key) { - // Hey, this is important. Listen up. - // - // This function pulls out only the **PUBLIC** pieces of this key. - // For RSA, that's the `e` and `n` values. - // - // BE CAREFUL IF YOU REFACTOR THIS. Thanks. - return { - kty: key.kty, - alg: 'RS256', - kid: key.kid, - 'fxa-createdAt': key['fxa-createdAt'], - use: 'sig', - n: key.n, - e: key.e - }; - } - - var keys = [pub(priv)]; - var old = config.get('openid.oldKey'); - if (Object.keys(old).length) { - keys.push(pub(old)); - } - return { keys: keys }; -})(); - -module.exports = { - handler: async function jwks() { - return KEYS; - } -}; diff --git a/fxa-oauth-server/lib/routes/key_data.js b/fxa-oauth-server/lib/routes/key_data.js deleted file mode 100644 index 093f69f..0000000 --- a/fxa-oauth-server/lib/routes/key_data.js +++ /dev/null @@ -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/. */ - -const Joi = require('joi'); - -const AppError = require('../error'); -const db = require('../db'); -const logger = require('../logging')('routes.key_data'); -const P = require('../promise'); -const validators = require('../validators'); -const verify = require('../browserid'); -const ScopeSet = require('fxa-shared').oauth.scopes; -const config = require('../config'); - -const AUTH_EXPIRES_AFTER_MS = config.get('expiration.keyDataAuth'); - -/** - * We're using a static value for key material on purpose, in future this value can read from the DB. - * @type {String} - */ -const KEY_ROTATION_SECRET = Buffer.alloc(32).toString('hex'); - -module.exports = { - validate: { - payload: { - client_id: validators.clientId, - assertion: validators.assertion.required(), - scope: validators.scope - } - }, - response: { - schema: Joi.object().pattern(/^/, [ - Joi.object({ - identifier: Joi.string().required(), - keyRotationSecret: Joi.string().required(), - keyRotationTimestamp: Joi.number().required() - }) - ]) - }, - handler: async function keyDataRoute(req) { - logger.debug('keyDataRoute.start', { - params: req.params, - payload: req.payload - }); - - const requestedScopes = ScopeSet.fromString(req.payload.scope); - const requestedClientId = req.payload.client_id; - - return P.all([ - verify(req.payload.assertion), - db.getClient(Buffer.from(requestedClientId, 'hex')).then((client) => { - if (client) { - // find all requested scopes that are allowed for this client. - const allowedScopes = ScopeSet.fromString(client.allowedScopes); - const scopeLookups = requestedScopes.filtered(allowedScopes).getScopeValues().map(s => db.getScope(s)); - return P.all(scopeLookups).then((result) => { - return result.filter((s) => !! (s && s.hasScopedKeys)); - }); - } else { - logger.debug('keyDataRoute.clientNotFound', { id: req.payload.client_id }); - throw AppError.unknownClient(requestedClientId); - } - }) - ]).then((results => { - logger.debug('keyDataRoute.results', JSON.stringify(results)); - const assertionData = results[0]; - const scopeResults = results[1]; - const response = {}; - - if (assertionData['fxa-lastAuthAt'] < (Date.now() - AUTH_EXPIRES_AFTER_MS) / 1000) { - throw AppError.staleAuthAt(assertionData['fxa-lastAuthAt']); - } - - scopeResults.forEach((keyScope) => { - response[keyScope.scope] = { - identifier: keyScope.scope, - keyRotationSecret: KEY_ROTATION_SECRET, - keyRotationTimestamp: assertionData['fxa-generation'] - }; - }); - - return response; - })); - - } -}; diff --git a/fxa-oauth-server/lib/routes/redirect.js b/fxa-oauth-server/lib/routes/redirect.js deleted file mode 100644 index 7215ce9..0000000 --- a/fxa-oauth-server/lib/routes/redirect.js +++ /dev/null @@ -1,62 +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/. */ - -const url = require('url'); - -const config = require('../config'); -const AppError = require('../error'); - -const ACTION_TO_PATHNAMES = { - 'email': '', - 'signin': 'signin', - 'signup': 'signup', - 'force_auth': 'force_auth' -}; - -function actionToPathname(action) { - if (action === undefined) { - return ''; - } - - if (ACTION_TO_PATHNAMES.hasOwnProperty(action)) { - return ACTION_TO_PATHNAMES[action]; - } - - throw new Error('Bad action parameter'); -} - -module.exports = { - handler: async function redirectAuthorization(req, h) { - var redirect = url.parse(config.get('contentUrl'), true); - var err = false; - - try { - redirect.pathname += actionToPathname(req.query.action); - } catch (e) { - err = true; - throw AppError.invalidRequestParameter('action'); - } - - if (! err) { - if (req.query.action !== 'email') { - // only `action=email` is propagated as a hint - // to the content server to show the email-first - // flow. All other actions redirect to a named - // endpoint. - delete req.query.action; - } - - if (req.query.login_hint && ! req.query.email) { - req.query.email = req.query.login_hint; - delete req.query.login_hint; - } - - redirect.query = req.query; - - delete redirect.search; - delete redirect.path; - return h.redirect(url.format(redirect)); - } - } -}; diff --git a/fxa-oauth-server/lib/routes/root.js b/fxa-oauth-server/lib/routes/root.js deleted file mode 100644 index 280937b..0000000 --- a/fxa-oauth-server/lib/routes/root.js +++ /dev/null @@ -1,54 +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/. */ - -const exec = require('child_process').exec; -const path = require('path'); - -const version = require('../../package.json').version; -var commitHash, source; - -// See if config/version.json exists (part of rpm builds) -try { - var info = require('../../config/version.json'); - commitHash = info.version.hash; - source = info.version.source; -} catch (e) { - /* ignore */ -} - -module.exports = { - handler: async function index(req, h) { - function sendReply() { - return h.response({ - version: version, - commit: commitHash, - source: source - }).spaces(2).suffix('\n'); - } - - if (commitHash) { - return sendReply(); - } - - function runGitCmd() { - return new Promise((resolve) => { - // figure it out from .git - var gitDir = path.resolve(__dirname, '..', '..', '.git'); - exec('git rev-parse HEAD', { cwd: gitDir }, function(err, stdout) { - commitHash = stdout.replace(/\s+/, ''); - var configPath = path.join(gitDir, 'config'); - var cmd = 'git config --get remote.origin.url'; - exec(cmd, { env: { GIT_CONFIG: configPath, PATH: process.env.PATH }}, function(err, stdout) { - source = stdout.replace(/\s+/, ''); - return resolve(); - }); - }); - }); - } - - return runGitCmd().then((resp) => { - return sendReply(); - }); - } -}; diff --git a/fxa-oauth-server/lib/routes/token.js b/fxa-oauth-server/lib/routes/token.js deleted file mode 100644 index 73cd147..0000000 --- a/fxa-oauth-server/lib/routes/token.js +++ /dev/null @@ -1,510 +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/. */ - -// Hello, dear traveller! Please, turn back now. It's dangerous in here! - -/*jshint camelcase: false*/ -const crypto = require('crypto'); -const AppError = require('../error'); -const buf = require('buf').hex; -const hex = require('buf').to.hex; -const Joi = require('joi'); -const JwTool = require('fxa-jwtool'); -const ScopeSet = require('fxa-shared').oauth.scopes; - -const config = require('../config'); -const db = require('../db'); -const encrypt = require('../encrypt'); -const logger = require('../logging')('routes.token'); -const P = require('../promise'); -const util = require('../util'); -const validators = require('../validators'); - -const HEX_STRING = validators.HEX_STRING; - -const MAX_TTL_S = config.get('expiration.accessToken') / 1000; -const GRANT_AUTHORIZATION_CODE = 'authorization_code'; -const GRANT_REFRESH_TOKEN = 'refresh_token'; -const GRANT_JWT = 'urn:ietf:params:oauth:grant-type:jwt-bearer'; - -const JWT_AUD = config.get('publicUrl') + '/v1/token'; - -const SERVICE_CLIENTS = {}; -const SERVICE_JWTOOL = new JwTool(config.get('serviceClients').map(function(client) { - SERVICE_CLIENTS[client.jku] = client; - return client.jku; -})); - -const SCOPE_OPENID = ScopeSet.fromArray(['openid']); - -const ID_TOKEN_EXPIRATION = Math.floor(config.get('openid.ttl') / 1000); -const ID_TOKEN_ISSUER = config.get('openid.issuer'); -const ID_TOKEN_KEY = JwTool.JWK.fromObject(config.get('openid.key'), { - iss: ID_TOKEN_ISSUER -}); - -const REFRESH_LAST_USED_AT_UPDATE_AFTER_MS = config.get('refreshToken.updateAfter'); - -const BASIC_AUTH_REGEX = /^Basic\s+([a-z0-9+\/]+)$/i; - -const PAYLOAD_SCHEMA = Joi.object({ - - client_id: validators.clientId - .when('grant_type', { - is: GRANT_JWT, - then: Joi.forbidden() - }) - .when('$headers.authorization', { - is: Joi.string().required(), - then: Joi.forbidden() - }), - - client_secret: validators.clientSecret - .when('grant_type', { - is: GRANT_JWT, - then: Joi.forbidden() - }) - .when('code_verifier', { - is: Joi.string().required(), // if (typeof code_verifier === 'string') { - then: Joi.forbidden() - }) - .when('grant_type', { - is: GRANT_REFRESH_TOKEN, - then: Joi.optional() - }) - .when('$headers.authorization', { - is: Joi.string().required(), - then: Joi.forbidden() - }), - - code_verifier: validators.codeVerifier - .when('grant_type', { - is: GRANT_JWT, - then: Joi.forbidden() - }), - - redirect_uri: validators.redirectUri.optional(), - - grant_type: Joi.string() - .valid(GRANT_AUTHORIZATION_CODE, GRANT_REFRESH_TOKEN, GRANT_JWT) - .default(GRANT_AUTHORIZATION_CODE) - .optional(), - - ttl: Joi.number() - .positive() - .max(MAX_TTL_S) - .default(MAX_TTL_S) - .optional(), - - scope: Joi.alternatives().when('grant_type', { - is: GRANT_REFRESH_TOKEN, - then: validators.scope, - otherwise: Joi.optional() - }), - - code: Joi.string() - .length(config.get('unique.code') * 2) - .regex(validators.HEX_STRING) - .required() - .when('grant_type', { - is: GRANT_AUTHORIZATION_CODE, - otherwise: Joi.forbidden() - }), - - refresh_token: validators.token - .required() - .when('grant_type', { - is: GRANT_REFRESH_TOKEN, - otherwise: Joi.forbidden() - }), - - assertion: validators.assertion - .required() - .when('grant_type', { - is: GRANT_JWT, - otherwise: Joi.forbidden() - }) - -}); - -// No? Still want to press on? Well, OK. But you were warned. -// -// This route takes takes an authorization grant, and returns an -// access_token if everything matches up. -// -// Steps from start to finish: -// -// 1. Confirm grant credentials. -// - If grant type is authorization code or refresh token, first -// confirm the client credentials in `confirmClientSecret()`. -// - If grant type is authorization code, proceed to `confirmCode()`. -// - If grant type is refresh token, proceed to `confirmRefreshToken()`. -// - If grant type is a JWT, all information is in the JWT. So jump -// straight to `confirmJwt()`. -// 2. Generate tokens. -// - An options object is passed to `generateTokens()`. -// - An access_token is generated. -// - If grant type is authorization code, and it was created with -// offline access, a refresh_token is also generated. -// 3. The tokens are returned in the response payload. -module.exports = { - validate: { - headers: Joi.object({ - 'authorization': Joi.string().regex(BASIC_AUTH_REGEX).optional() - }).options({ allowUnknown: true }), - // stripUnknown is used to allow various oauth2 libraries to be used - // with FxA OAuth. Sometimes, they will send other parameters that - // we don't use, such as `response_type`, or something else. Instead - // of giving an error here, we can just ignore them. - payload: PAYLOAD_SCHEMA.options({ stripUnknown: true }) - }, - response: { - schema: Joi.object().keys({ - access_token: validators.token.required(), - refresh_token: validators.token, - id_token: validators.assertion, - scope: validators.scope.required().allow(''), - token_type: Joi.string().valid('bearer').required(), - expires_in: Joi.number().max(MAX_TTL_S).required(), - auth_at: Joi.number(), - keys_jwe: validators.jwe.optional() - }) - }, - handler: async function tokenEndpoint(req) { - var params = req.payload; - params.scope = ScopeSet.fromString(params.scope || ''); - return P.try(function() { - - // Clients are allowed to provide credentials in either - // the Authorization header or request body. Normalize. - const authzMatch = BASIC_AUTH_REGEX.exec(req.headers.authorization || ''); - if (authzMatch) { - const creds = Buffer.from(authzMatch[1], 'base64').toString().split(':'); - const err = new AppError.invalidRequestParameter('authorization'); - if (creds.length !== 2) { - throw err; - } - params.client_id = Joi.attempt(creds[0], validators.clientId, err); - params.client_secret = Joi.attempt(creds[1], validators.clientSecret, err); - } - - var clientId = params.client_id; - - if (params.grant_type === GRANT_AUTHORIZATION_CODE) { - return getClientById(clientId).then(function(client) { - if (params.code_verifier && validPublicClient(client)) { - return confirmPkceCode(params.code, params.code_verifier); - } else { - return confirmClientSecret(client, params.client_secret).then(function() { - return confirmCode(params.client_id, params.code); - }); - } - }); - } else if (params.grant_type === GRANT_REFRESH_TOKEN) { - // If the client has a client_secret, check that it's provided and valid in the refresh request. - // If the client does not have client_secret, check that one was not provided in the refresh request. - return getClientById(clientId).then(function(client) { - var confirmClientPromise; - - if (client.publicClient) { - if (params.client_secret) { - throw new AppError.invalidRequestParameter('client_secret'); - } - - confirmClientPromise = P.resolve(); - } else { - confirmClientPromise = confirmClientSecret(client, params.client_secret); - } - - return confirmClientPromise - .then(function() { - return confirmRefreshToken(params); - }); - }); - } else if (params.grant_type === GRANT_JWT) { - return confirmJwt(params); - } else { - // else our Joi validation failed us? - logger.critical('joi.grant_type', params.grant_type); - throw Error('unreachable'); - } - }) - .then(function(vals) { - vals.ttl = params.ttl; - if (vals.scope && vals.scope.contains(SCOPE_OPENID)) { - vals.idToken = true; - } - return vals; - }) - .then(generateTokens); - } -}; - -/** - * Generate a PKCE code_challenge - * See https://tools.ietf.org/html/rfc7636#section-4.6 for details - */ -function pkceHash(input) { - return util.base64URLEncode(crypto.createHash('sha256').update(input).digest()); -} - -function validPublicClient(client) { - if (! client.publicClient) { - logger.debug('client.notPublicClient', { id: client.id }); - throw AppError.notPublicClient(client.id); - } - - return true; -} - -function getClientById(clientId) { - return db.getClient(buf(clientId)).then(function(client) { - if (! client) { - logger.debug('client.notFound', { id: clientId }); - throw AppError.unknownClient(clientId); - } - - return client; - }); -} - -function confirmPkceCode(code, pkceVerifier) { - return db.getCode(buf(code)).then(function(codeObj) { - if (! codeObj) { - logger.debug('code.notFound', { code: code }); - throw AppError.unknownCode(code); - } - - const pkceHashValue = pkceHash(pkceVerifier); - if (codeObj.codeChallenge && - codeObj.codeChallengeMethod === 'S256' && - pkceHashValue === codeObj.codeChallenge) { - return db.removeCode(buf(code)).then(function() { - return codeObj; - }); - } else { - throw AppError.mismatchCodeChallenge(pkceHashValue); - } - }); -} - -function confirmClientSecret(client, secret) { - return P.resolve().then(function() { - var id = client.id; - var submitted = hex(encrypt.hash(buf(secret))); - var stored = hex(client.hashedSecret); - - if (submitted !== stored) { - var storedPrevious; - if (client.hashedSecretPrevious) { - // Check if secret used is the current previous secret - storedPrevious = hex(client.hashedSecretPrevious); - if (submitted === storedPrevious) { - logger.info('client.matchSecretPrevious', { client: id }); - return client; - } - } - - logger.info('client.mismatchSecret', { client: id }); - logger.verbose('client.mismatchSecret.details', { - submitted: submitted, - db: stored, - dbPrevious: storedPrevious - }); - throw AppError.incorrectSecret(id); - } - - return client; - }); -} - -function confirmCode(id, code) { - return db.getCode(buf(code)).then(function(codeObj) { - if (! codeObj) { - logger.debug('code.notFound', { code: code }); - throw AppError.unknownCode(code); - } else if (hex(codeObj.clientId) !== hex(id)) { - logger.debug('code.mismatch', { - client: hex(id), - code: hex(codeObj.clientId) - }); - throw AppError.mismatchCode(code, id); - } else { - // + because loldatemath. without it, it does string concat - var expiresAt = +codeObj.createdAt + config.get('expiration.code'); - if (Date.now() > expiresAt) { - logger.debug('code.expired', { code: code }); - throw AppError.expiredCode(code, expiresAt); - } - } - return db.removeCode(buf(code)).then(function() { - return codeObj; - }); - }); -} - -function confirmRefreshToken(params) { - return db.getRefreshToken(encrypt.hash(params.refresh_token)) - .then(function(tokObj) { - if (! tokObj) { - logger.debug('refresh_token.notFound', params.refresh_token); - throw AppError.invalidToken(); - } else if (hex(tokObj.clientId) !== hex(params.client_id)) { - logger.debug('refresh_token.mismatch', { - client: params.client_id, - code: tokObj.clientId - }); - throw AppError.invalidToken(); - } else if (! tokObj.scope.contains(params.scope)) { - logger.debug('refresh_token.invalidScopes', { - allowed: tokObj.scope, - requested: params.scope - }); - throw AppError.invalidScopes(); - } - tokObj.scope = params.scope; - - var now = new Date(); - var lastUsedAt = tokObj.lastUsedAt; - - if ((now - lastUsedAt) > REFRESH_LAST_USED_AT_UPDATE_AFTER_MS){ - db.usedRefreshToken(encrypt.hash(params.refresh_token)).then(function() { - logger.debug('usedRefreshToken.updated', now); - }); - } else { - logger.debug('usedRefreshToken.not_updated'); - } - return tokObj; - }); -} - -function confirmJwt(params) { - var assertion = params.assertion; - logger.debug('jwt.confirm', assertion); - - return SERVICE_JWTOOL.verify(assertion).catch(function(err) { - logger.info('jwt.invalid.verify', err.message); - throw AppError.invalidAssertion(); - }).then(function(payload) { - logger.verbose('jwt.payload', payload); - - // this cannot fail! huh, why? - // if the assertion couldn't decode, or the jku was not in our - // trusted list, the assertion would not have verified. - var client = SERVICE_CLIENTS[JwTool.unverify(assertion).header.jku]; - - // ohai eslint complexity - var uid = _validateJwtSub(payload.sub); - - if (payload.aud !== JWT_AUD) { - logger.debug('jwt.invalid.aud', payload.aud); - throw AppError.invalidAssertion(); - } - - const requestedScope = ScopeSet.fromString(payload.scope); - if (! ScopeSet.fromString(client.scope).contains(requestedScope)) { - logger.debug('jwt.invalid.scopes', { - allowed: client.scope, - requested: payload.scope - }); - throw AppError.invalidScopes(payload.scope); - } - - var now = Date.now() / 1000; - if ((payload.iat || Infinity) > now) { - logger.debug('jwt.invalid.iat', { now: now, iat: payload.iat }); - throw AppError.invalidAssertion(); - } - if ((payload.exp || -Infinity) < now) { - logger.debug('jwt.invalid.exp', { now: now, exp: payload.exp }); - throw AppError.invalidAssertion(); - } - - // We can't know the email based on a service token, - // so we can't cache it locally. Insert an empty string - // for now, while we complete the process of entirely - // removing the 'email' column from our database. - return { - clientId: client.id, - userId: uid, - scope: requestedScope, - email: '' - }; - }); -} - -function _validateJwtSub(sub) { - if (! sub) { - logger.debug('jwt.invalid.sub.missing'); - throw AppError.invalidAssertion(); - } - if (sub.length !== 32 || ! HEX_STRING.test(sub)) { - logger.debug('jwt.invalid.sub', sub); - throw AppError.invalidAssertion(); - } - - return sub; -} - -function generateIdToken(options, access) { - var now = Math.floor(Date.now() / 1000); - var claims = { - sub: hex(options.userId), - aud: hex(options.clientId), - iss: ID_TOKEN_ISSUER, - iat: now, - exp: now + ID_TOKEN_EXPIRATION, - at_hash: util.generateTokenHash(access.token) - }; - if (options.amr) { - claims.amr = options.amr; - } - if (options.aal) { - claims['fxa-aal'] = options.aal; - claims.acr = 'AAL' + options.aal; - } - - return ID_TOKEN_KEY.sign(claims); -} - -function generateTokens (options) { - // we always are generating an access token here - // but depending on options, we may also be generating a refresh_token - return db.generateAccessToken(options) - .then((access) => { - const promises = {}; - if (options.offline) { - promises.refresh = db.generateRefreshToken(options); - } - if (options.idToken) { - promises.idToken = generateIdToken(options, access); - } - - return P.props(promises).then(function (result) { - const refresh = result.refresh; - const idToken = result.idToken; - - const json = { - access_token: access.token.toString('hex'), - token_type: access.type, - scope: access.scope.toString() - }; - if (options.authAt) { - json.auth_at = options.authAt; - } - json.expires_in = options.ttl; - if (refresh) { - json.refresh_token = refresh.token.toString('hex'); - } - if (idToken) { - json.id_token = idToken; - } - if (options.keysJwe) { - json.keys_jwe = options.keysJwe; - } - return json; - }); - }); -} - diff --git a/fxa-oauth-server/lib/routes/verify.js b/fxa-oauth-server/lib/routes/verify.js deleted file mode 100644 index 38c4668..0000000 --- a/fxa-oauth-server/lib/routes/verify.js +++ /dev/null @@ -1,45 +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/. */ - -const Joi = require('joi'); - -const logger = require('../logging')('routes.verify'); -const token = require('../token'); -const validators = require('../validators'); - -module.exports = { - validate: { - payload: { - token: validators.token.required(), - email: Joi.boolean().optional() - } - }, - response: { - schema: { - user: Joi.string().required(), - client_id: Joi.string().required(), - scope: Joi.array(), - email: Joi.string(), - profile_changed_at: Joi.number().min(0) - } - }, - handler: async function verify(req) { - return token.verify(req.payload.token).then(function(info) { - info.scope = info.scope.getScopeValues(); - if (req.payload.email !== undefined) { - logger.warn('email.requested', { - user: info.user, - client_id: info.client_id, - scope: info.scope - }); - } - delete info.email; - logger.info('verify.success', { - client_id: info.client_id, - scope: info.scope - }); - return info; - }); - } -}; diff --git a/fxa-oauth-server/lib/routing.js b/fxa-oauth-server/lib/routing.js deleted file mode 100644 index c2c14a8..0000000 --- a/fxa-oauth-server/lib/routing.js +++ /dev/null @@ -1,123 +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/. */ - -const config = require('./config'); -const version = config.get('api.version'); - -function v(url) { - return '/v' + version + url; -} - -exports.routes = [ - { - method: 'GET', - path: '/', - config: require('./routes/root') - }, - { - method: 'GET', - path: '/__version__', - config: require('./routes/root') - }, - { - method: 'GET', - path: '/__heartbeat__', - config: require('./routes/heartbeat') - }, - { - method: 'GET', - path: '/__lbheartbeat__', - config: require('./routes/heartbeat') - }, - { - method: 'GET', - path: '/config', - config: require('./routes/config') - }, - - // v1 API - { - method: 'GET', - path: v('/client/{client_id}'), - config: require('./routes/client/get') - }, - { - method: 'POST', - path: v('/key-data'), - config: require('./routes/key_data') - }, - { - method: 'GET', - path: v('/authorization'), - config: require('./routes/redirect') - }, - { - method: 'POST', - path: v('/authorization'), - config: require('./routes/authorization') - }, - { - method: 'POST', - path: v('/token'), - config: require('./routes/token') - }, - { - method: 'POST', - path: v('/destroy'), - config: require('./routes/destroy') - }, - { - method: 'POST', - path: v('/verify'), - config: require('./routes/verify') - }, - { - method: 'GET', - path: v('/jwks'), - config: require('./routes/jwks') - }, - { - method: 'GET', - path: v('/client-tokens'), - config: require('./routes/client-tokens/list') - }, - { - method: 'DELETE', - path: v('/client-tokens/{client_id}'), - config: require('./routes/client-tokens/delete') - } -]; - -exports.clients = [ - { - method: 'GET', - path: v('/client/{client_id}'), - config: require('./routes/client/get') - }, - { - method: 'GET', - path: v('/clients'), - config: require('./routes/client/list') - }, - { - method: 'POST', - path: v('/client'), - config: require('./routes/client/register') - }, - { - method: 'POST', - path: v('/client/{client_id}'), - config: require('./routes/client/update') - }, - { - method: 'DELETE', - path: v('/client/{client_id}'), - config: require('./routes/client/delete') - }, - { - method: 'POST', - path: v('/developer/activate'), - config: require('./routes/developer/activate') - } -]; diff --git a/fxa-oauth-server/lib/server/config.js b/fxa-oauth-server/lib/server/config.js deleted file mode 100644 index ec7b666..0000000 --- a/fxa-oauth-server/lib/server/config.js +++ /dev/null @@ -1,37 +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/. */ - -const config = require('../config').getProperties(); - -module.exports = { - host: config.server.host, - port: config.server.port, - routes: { - cache: { - otherwise: config.cacheControl - }, - cors: true, - payload: { - maxBytes: 16384 - }, - security: { - hsts: { - maxAge: 15552000, - includeSubdomains: true - }, - xframe: true, - xss: true, - noOpen: false, - noSniff: true - }, - validate: { - failAction: async (request, h, err) => { - // Starting with Hapi 17, the framework hides the validation info - // We want the full validation information and use it in server `onPreResponse` - // See: https://github.com/hapijs/hapi/issues/3706#issuecomment-349765943 - throw err; - } - }, - } -}; diff --git a/fxa-oauth-server/lib/server/index.js b/fxa-oauth-server/lib/server/index.js deleted file mode 100644 index 46e920e..0000000 --- a/fxa-oauth-server/lib/server/index.js +++ /dev/null @@ -1,150 +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/. */ - -const Hapi = require('hapi'); -const Raven = require('raven'); - -const AppError = require('../error'); -const authBearer = require('../auth_bearer'); -const config = require('../config').getProperties(); -const env = require('../env'); -const logger = require('../logging')('server'); -const hapiLogger = require('../logging')('server.hapi'); -const summary = require('../logging/summary'); - -exports.create = async function createServer() { - - if (config.localRedirects && config.env !== 'dev') { - // nightly, latest, etc will probably set this to true, but it's - // worth explicitly yelling about it. - logger.warn('localRedirect', - '*** localRedirects is set to TRUE. Should only be used for developers.'); - } - var isProd = env.isProdLike(); - var server = new Hapi.Server( - require('./config') - ); - - server.auth.scheme(authBearer.AUTH_SCHEME, authBearer.strategy); - server.auth.strategy(authBearer.AUTH_STRATEGY, authBearer.AUTH_SCHEME); - - if (config.hpkpConfig && config.hpkpConfig.enabled) { - var hpkpOptions = { - maxAge: config.hpkpConfig.maxAge, - sha256s: config.hpkpConfig.sha256s, - includeSubdomains: config.hpkpConfig.includeSubDomains - }; - - if (config.hpkpConfig.reportUri){ - hpkpOptions.reportUri = config.hpkpConfig.reportUri; - } - - if (config.hpkpConfig.reportOnly){ - hpkpOptions.reportOnly = config.hpkpConfig.reportOnly; - } - - await server.register({ - plugin: require('hapi-hpkp'), - options: hpkpOptions - }); - } - - var routes = require('../routing').routes; - if (isProd) { - logger.info('prod', 'Disabling response schema validation'); - routes.forEach(function(route) { - delete route.config.response; - }); - } - - // default to stricter content-type - routes.forEach(function(route) { - var method = route.method.toUpperCase(); - if (method !== 'GET' && method !== 'HEAD') { - if (! route.config.payload) { - route.config.payload = { - allow: ['application/json', 'application/x-www-form-urlencoded'] - }; - } - logger.verbose('route.payload', { - path: route.path, - method: method, - payload: route.config.payload - }); - } - }); - - server.route(routes); - - // hapi internal logging: server and request - server.events.on('log', function onServerLog(ev, tags) { - if (tags.error && tags.implementation) { - hapiLogger.critical('error.uncaught.server', ev.data); - } - }); - - server.events.on('request', function onRequestLog(req, ev, tags) { - if (tags.error && tags.implementation) { - if (ev.data.stack.indexOf('hapi/lib/validation.js') !== -1) { - hapiLogger.error('error.payload.validation', ev.data); - } else { - hapiLogger.critical('error.uncaught.request', ev.data); - } - } - }); - - // configure Sentry - const sentryDsn = config.sentryDsn; - if (sentryDsn) { - Raven.config(sentryDsn, {}); - server.events.on({ name: 'request', channel: 'error' }, function (req, ev) { - const err = ev && ev.error || null; - let exception = ''; - if (err && err.stack) { - try { - exception = err.stack.split('\n')[0]; - } catch (e) { - // ignore bad stack frames - } - } - - Raven.captureException(err, { - extra: { - exception: exception - } - }); - }); - } - - server.ext('onPreResponse', function onPreResponse(request, h) { - var response = request.response; - if (response.isBoom) { - response = AppError.translate(response); - } - summary(request, response); - - return response; - }); - - server.ext('onPreAuth', function (request, h) { - // Construct source-ip-address chain for logging. - var xff = (request.headers['x-forwarded-for'] || '').split(/\s*,\s*/); - xff.push(request.info.remoteAddress); - // Remove empty items from the list, in case of badly-formed header. - xff = xff.filter(function(x){ - return x; - }); - // Skip over entries for our own infra, loadbalancers, etc. - var clientAddressIndex = xff.length - (config.clientAddressDepth || 1); - if (clientAddressIndex < 0) { - clientAddressIndex = 0; - } - - request.app.remoteAddressChain = xff; - request.app.clientAddress = xff[clientAddressIndex]; - return h.continue; - }); - - return server; -}; diff --git a/fxa-oauth-server/lib/server/internal.js b/fxa-oauth-server/lib/server/internal.js deleted file mode 100644 index 29fa5d3..0000000 --- a/fxa-oauth-server/lib/server/internal.js +++ /dev/null @@ -1,98 +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/. */ - -const Hapi = require('hapi'); - -const AppError = require('../error'); -const auth = require('../auth'); -const config = require('../config').getProperties(); -const env = require('../env'); -const logger = require('../logging')('server.clients'); -const hapiLogger = require('../logging')('server.hapi'); -const summary = require('../logging/summary'); - -exports.create = async function createServer() { - var isProd = env.isProdLike(); - - const serverConfig = require('./config'); - serverConfig.host = config.serverInternal.host; - serverConfig.port = config.serverInternal.port; - - var server = new Hapi.Server(serverConfig); - - server.auth.scheme(auth.AUTH_SCHEME, auth.strategy); - server.auth.strategy(auth.AUTH_STRATEGY, auth.AUTH_SCHEME); - - if (config.hpkpConfig && config.hpkpConfig.enabled) { - var hpkpOptions = { - maxAge: config.hpkpConfig.maxAge, - sha256s: config.hpkpConfig.sha256s, - includeSubdomains: config.hpkpConfig.includeSubDomains - }; - - if (config.hpkpConfig.reportUri){ - hpkpOptions.reportUri = config.hpkpConfig.reportUri; - } - - if (config.hpkpConfig.reportOnly){ - hpkpOptions.reportOnly = config.hpkpConfig.reportOnly; - } - - await server.register({ - plugin: require('hapi-hpkp'), - options: hpkpOptions - }); - } - - var routes = require('../routing').clients; - if (isProd) { - logger.info('prod', 'Disabling response schema validation'); - routes.forEach(function(route) { - delete route.config.response; - }); - } - - // default to stricter content-type - routes.forEach(function(route) { - var method = route.method.toUpperCase(); - if (method !== 'GET' && method !== 'HEAD') { - if (! route.config.payload) { - route.config.payload = { - allow: ['application/json', 'application/x-www-form-urlencoded'] - }; - } - logger.verbose('route.payload', { - path: route.path, - method: method, - payload: route.config.payload - }); - } - }); - - server.route(routes); - - // hapi internal logging: server and request - server.events.on('log', function onServerLog(ev, tags) { - if (tags.error && tags.implementation) { - hapiLogger.critical('error.uncaught', { tags: ev.tags, error: ev.data }); - } - }); - - server.events.on('request', function onRequestLog(req, ev, tags) { - if (tags.error && tags.implementation) { - hapiLogger.critical('error.uncaught', { tags: ev.tags, error: ev.data }); - } - }); - - server.ext('onPreResponse', function onPreResponse(request, h) { - var response = request.response; - if (response.isBoom) { - response = AppError.translate(response); - } - summary(request, response); - return response; - }); - - return server; -}; diff --git a/fxa-oauth-server/lib/token.js b/fxa-oauth-server/lib/token.js deleted file mode 100644 index 5d56a2b..0000000 --- a/fxa-oauth-server/lib/token.js +++ /dev/null @@ -1,70 +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/. */ - -const ScopeSet = require('fxa-shared').oauth.scopes; - -const AppError = require('./error'); -const config = require('./config'); -const db = require('./db'); -const encrypt = require('./encrypt'); -const logger = require('./logging')('token'); - -const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000; - -const SCOPES_REQUIRING_EMAIL = ScopeSet.fromArray(['profile:email', 'oauth']); - -exports.verify = function verify(token) { - return db.getAccessToken(encrypt.hash(token)) - .then(function(token) { - if (! token) { - throw AppError.invalidToken(); - } else if (+token.expiresAt < Date.now()) { - // We dug ourselves a bit of a hole with token expiry, - // and this logic is here to help us climb back out. - // There's a huge backlog of expired tokens in the wild, - // and if we start rejecting them all at once, then the - // thundering herd of token updates will crush our db. - // Instead we "grandfather" these old tokens in and - // pretend they're still valid, while chipping away at - // the backlog by either slowly reducing this epoch, or - // by slowly purging older tokens from the db. - if (+token.expiresAt >= config.get('expiration.accessTokenExpiryEpoch')) { - throw AppError.expiredToken(token.expiresAt); - } - logger.warn('token.verify.expired', { - user: token.userId.toString('hex'), - client_id: token.clientId.toString('hex'), - scope: token.scope.toString(), - created_at: token.createdAt, - expires_at: token.expiresAt - }); - } else if ((+token.createdAt + TWENTY_FOUR_HOURS) < Date.now()) { - // Log a warning if reliers are using access tokens that are more - // than 24 hours old. Eventually we will shorten the expiry time - // on access tokens and such old tokens won't be allowed. - logger.warn('token.verify.expiring_soon', { - user: token.userId.toString('hex'), - client_id: token.clientId.toString('hex'), - scope: token.scope, - created_at: token.createdAt, - expires_at: token.expiresAt - }); - } - var tokenInfo = { - user: token.userId.toString('hex'), - client_id: token.clientId.toString('hex'), - scope: token.scope - }; - - if (token.profileChangedAt) { - tokenInfo.profile_changed_at = token.profileChangedAt; - } - - if (token.scope.intersects(SCOPES_REQUIRING_EMAIL)) { - tokenInfo.email = token.email; - } - - return tokenInfo; - }); -}; diff --git a/fxa-oauth-server/lib/unique.js b/fxa-oauth-server/lib/unique.js deleted file mode 100644 index e6434af..0000000 --- a/fxa-oauth-server/lib/unique.js +++ /dev/null @@ -1,25 +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/. */ - -const crypto = require('crypto'); - -const config = require('./config'); - -function unique(length) { - return crypto.randomBytes(length); // eslint-disable-line fxa/async-crypto-random -} - -function fn(configName) { - return function udid() { - return unique(config.get('unique.' + configName)); - }; -} - -unique.id = fn('id'); -unique.developerId = fn('developerId'); -unique.secret = fn('clientSecret'); -unique.code = fn('code'); -unique.token = fn('token'); - -module.exports = unique; diff --git a/fxa-oauth-server/lib/util.js b/fxa-oauth-server/lib/util.js deleted file mode 100644 index 6fbfe6e..0000000 --- a/fxa-oauth-server/lib/util.js +++ /dev/null @@ -1,46 +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/. */ - -const encrypt = require('./encrypt'); - -/** - * .base64URLEncode - * - * return an encoded Buffer as URL Safe Base64 - * - * Note: This function encodes to the RFC 4648 Spec where '+' is encoded - * as '-' and '/' is encoded as '_'. The padding character '=' is - * removed. - * - * @param {Buffer} buf - * @return {String} - * @api public - */ -const base64URLEncode = function base64URLEncode(buf) { - return buf.toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); -}; - -/** - * Generates a hash of the access token based on - * http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken - * - * This value is the hash of the ascii value of the access token, then the base64url - * value of the left half. - * - * @param {Buffer} accessTokenBuf - * @returns {String} - * @api public - */ -const generateTokenHash = function generateTokenHash (accessTokenBuf) { - const hash = encrypt.hash(accessTokenBuf.toString('ascii')); - return base64URLEncode(hash.slice(0, hash.length / 2)); -}; - -module.exports = { - base64URLEncode: base64URLEncode, - generateTokenHash: generateTokenHash -}; diff --git a/fxa-oauth-server/lib/validators.js b/fxa-oauth-server/lib/validators.js deleted file mode 100644 index 116ab63..0000000 --- a/fxa-oauth-server/lib/validators.js +++ /dev/null @@ -1,48 +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/. */ - -const Joi = require('joi'); - -const config = require('./config'); - -exports.HEX_STRING = /^(?:[0-9a-f]{2})+$/; -exports.B64URL_STRING = /^[A-Za-z0-9-_]+$/; - -exports.clientId = Joi.string() - .length(config.get('unique.id') * 2) // hex = bytes*2 - .regex(exports.HEX_STRING) - .required(); - -exports.clientSecret = Joi.string() - .length(config.get('unique.clientSecret') * 2) // hex = bytes*2 - .regex(exports.HEX_STRING) - .required(); - -exports.codeVerifier = Joi.string() - .min(43) - .max(128) - .regex(exports.B64URL_STRING); // https://tools.ietf.org/html/rfc7636#section-4.1 - -exports.token = Joi.string() - .length(config.get('unique.token') * 2) - .regex(exports.HEX_STRING); - -exports.scope = Joi.string() - .max(256) - .regex(/^[a-zA-Z0-9 _\/.:-]+$/); - -exports.redirectUri = Joi.string() - .max(256) - .regex(/^[a-zA-Z0-9\-_\/.:?=&]+$/); - -// taken from mozilla/persona/lib/validate.js -exports.assertion = Joi.string() - .min(50) - .max(10240) - .regex(/^[a-zA-Z0-9_\-\.~=]+$/); - -exports.jwe = Joi.string() - .max(1024) - // JWE token format: 'protectedheader.encryptedkey.iv.cyphertext.authenticationtag' - .regex(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/); diff --git a/fxa-oauth-server/npm-shrinkwrap.json b/fxa-oauth-server/npm-shrinkwrap.json deleted file mode 100644 index ce1c492..0000000 --- a/fxa-oauth-server/npm-shrinkwrap.json +++ /dev/null @@ -1,8834 +0,0 @@ -{ - "name": "fxa-oauth-server", - "version": "1.123.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/generator": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.1.3.tgz", - "integrity": "sha512-ZoCZGcfIJFJuZBqxcY9OjC1KW2lWK64qrX1o4UYL3yshVhwKFYgzpWZ0vvtGMNJdTlvkw0W+HR1VnYN8q3QPFQ==", - "dev": true, - "requires": { - "@babel/types": "^7.1.3", - "jsesc": "^2.5.1", - "lodash": "^4.17.10", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "@babel/helper-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz", - "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.1.3.tgz", - "integrity": "sha512-gqmspPZOMW3MIRb9HlrnbZHXI1/KHTOroBwN1NcLL6pWxzqzEKGvRTq0W/PxS45OtQGbaFikSQpkS5zbnsQm2w==", - "dev": true - }, - "@babel/template": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.1.2.tgz", - "integrity": "sha512-SY1MmplssORfFiLDcOETrW7fCLl+PavlwMh92rrGcikQaRq4iWPVH0MpwPpY3etVMx6RnDjXtr6VZYr/IbP/Ag==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.1.2", - "@babel/types": "^7.1.2" - } - }, - "@babel/traverse": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.1.4.tgz", - "integrity": "sha512-my9mdrAIGdDiSVBuMjpn/oXYpva0/EZwWL3sm3Wcy/AVWO2eXnsoZruOT9jOGNRXU8KbCIu5zsKnXcAJ6PcV6Q==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.1.3", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.0.0", - "@babel/parser": "^7.1.3", - "@babel/types": "^7.1.3", - "debug": "^3.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.10" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "globals": { - "version": "11.8.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.8.0.tgz", - "integrity": "sha512-io6LkyPVuzCHBSQV9fmOwxZkUk6nIaGmxheLDgmuFv89j0fm2aqDbIXKAGfzCMHqz3HLF2Zf8WSG6VqMh2qFmA==", - "dev": true - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.1.3.tgz", - "integrity": "sha512-RpPOVfK+yatXyn8n4PB1NW6k9qjinrXrRR8ugBN8fD6hCy5RXI6PSbVqpOJBO9oSaY7Nom4ohj35feb0UR9hSA==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.10", - "to-fast-properties": "^2.0.0" - } - }, - "@newrelic/native-metrics": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@newrelic/native-metrics/-/native-metrics-2.4.0.tgz", - "integrity": "sha512-6Pv2Z9vkinr0MTnH1BORBs/SFOdKei43tQo2z30h9NtTc1pmWb/n5VWjgp7ReZ7FwzTI2oIhjbgnk2gZzpl6bw==", - "optional": true, - "requires": { - "nan": "^2.8.0" - } - }, - "@sinonjs/commons": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.0.2.tgz", - "integrity": "sha512-WR3dlgqJP4QNrLC4iXN/5/2WaLQQ0VijOOkmflqFGVJ6wLEpbSjo7c0ZeGIdtY8Crk7xBBp87sM6+Mkerz7alw==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - }, - "dependencies": { - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - } - } - }, - "@sinonjs/formatio": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.0.0.tgz", - "integrity": "sha512-vdjoYLDptCgvtJs57ULshak3iJe4NW3sJ3g36xVDGff5AE8P30S6A093EIEPjdi2noGhfuNOEkbxt3J3awFW1w==", - "dev": true, - "requires": { - "@sinonjs/samsam": "2.1.0" - }, - "dependencies": { - "@sinonjs/samsam": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.0.tgz", - "integrity": "sha512-5x2kFgJYupaF1ns/RmharQ90lQkd2ELS8A9X0ymkAAdemYHGtI2KiUHG8nX2WU0T1qgnOU5YMqnBM2V7NUanNw==", - "dev": true, - "requires": { - "array-from": "^2.1.1" - } - } - } - }, - "@sinonjs/samsam": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.2.tgz", - "integrity": "sha512-ZwTHAlC9akprWDinwEPD4kOuwaYZlyMwVJIANsKNC3QVp0AHB04m7RnB4eqeWfgmxw8MGTzS9uMaw93Z3QcZbw==", - "dev": true - }, - "JSONStream": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.3.tgz", - "integrity": "sha512-3Sp6WZZ/lXl+nTDoGpGWHEpTnnC6X5fnkolYZR6nwIfzbxxvA8utPWe1gCt7i0m9uVGsSz2IS8K8mJ7HmlduMg==", - "dev": true, - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "accept": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/accept/-/accept-3.0.2.tgz", - "integrity": "sha512-bghLXFkCOsC1Y2TZ51etWfKDs6q249SAoHTZVfzWWdlZxoij+mgkj9AmUJWQpDY48TfnrTDIe43Xem4zdMe7mQ==", - "requires": { - "boom": "7.x.x", - "hoek": "5.x.x" - }, - "dependencies": { - "boom": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-7.2.0.tgz", - "integrity": "sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY=", - "requires": { - "hoek": "5.x.x" - } - }, - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - } - } - }, - "accept-language": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/accept-language/-/accept-language-2.0.17.tgz", - "integrity": "sha1-Q/CRtMgUSVV/e3d9iApRAQUwwGA=", - "requires": { - "bcp47": "^1.1.2" - } - }, - "acorn": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz", - "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==", - "dev": true - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "^3.0.4" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } - }, - "agent-base": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-1.0.2.tgz", - "integrity": "sha1-aJDT+yFwBLYrcPiSjg+uX4lSpwY=" - }, - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "ajv-keywords": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", - "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", - "dev": true - }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dev": true, - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "ammo": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ammo/-/ammo-3.0.1.tgz", - "integrity": "sha512-4UqoM8xQjwkQ78oiU4NbBK0UgYqeKMAKmwE4ec7Rz3rGU8ZEBFxzgF2sUYKOAlqIXExBDYLN6y1ShF5yQ4hwLQ==", - "requires": { - "hoek": "5.x.x" - }, - "dependencies": { - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - } - } - }, - "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", - "dev": true, - "requires": { - "string-width": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-differ": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", - "dev": true - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true - }, - "array-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", - "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", - "dev": true - }, - "array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "asn1.js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-1.0.3.tgz", - "integrity": "sha1-KBuj7B8kSP52X5Kk7s+IP+E2S1Q=", - "requires": { - "bn.js": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" - }, - "async-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "atob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", - "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", - "dev": true - }, - "aws-sdk": { - "version": "2.0.23", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.0.23.tgz", - "integrity": "sha1-viHKuIYJkYaZ55s4DgYOAvTIm6k=", - "requires": { - "xml2js": "0.2.6", - "xmlbuilder": "0.4.2" - } - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" - }, - "b64": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/b64/-/b64-4.0.0.tgz", - "integrity": "sha512-EhmUQodKB0sdzPPrbIWbGqA5cQeTWxYrAgNeeT1rLZWtD3tbNTnphz8J4vkXI3cPgBNlXBjzEbzDzq0Nwi4f9A==" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "bcp47": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/bcp47/-/bcp47-1.1.2.tgz", - "integrity": "sha1-NUvjMH/9CEM6ePXh4glYRfifx/4=" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "big-time": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/big-time/-/big-time-2.0.1.tgz", - "integrity": "sha1-aMffjcMPl+lT8lpnp2rJcTwWyd4=" - }, - "bignumber.js": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-4.0.4.tgz", - "integrity": "sha512-LDXpJKVzEx2/OqNbG9mXBNvHuiRL4PzHCGfnANHMJ+fv68Ads3exDVJeGDJws+AoNEuca93bU3q+S0woeUaCdg==" - }, - "binary-extensions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", - "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", - "dev": true - }, - "bluebird": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.2.tgz", - "integrity": "sha512-dhHTWMI7kMx5whMQntl7Vr9C6BvV10lFXDAasnqnrMYhXVCzzk6IO9Fo2L75jXHT07WrOngL1WDXOp+yYS91Yg==" - }, - "bn.js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-1.3.0.tgz", - "integrity": "sha1-DbTL+W+PI7dC9by50ap6mZSgXoM=", - "optional": true - }, - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "requires": { - "hoek": "4.x.x" - } - }, - "bounce": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/bounce/-/bounce-1.2.0.tgz", - "integrity": "sha512-8syCGe8B2/WC53118/F/tFy5aW00j+eaGPXmAUP7iBhxc+EBZZxS1vKelWyBCH6IqojgS2t1gF0glH30qAJKEw==", - "requires": { - "boom": "7.x.x", - "hoek": "5.x.x" - }, - "dependencies": { - "boom": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-7.2.0.tgz", - "integrity": "sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY=", - "requires": { - "hoek": "5.x.x" - } - }, - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - } - } - }, - "boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", - "dev": true, - "requires": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "buf": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/buf/-/buf-0.1.0.tgz", - "integrity": "sha1-o0/Kie5qQdMmFmhl4Qg/oRLONGg=" - }, - "buffer-from": { - "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", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "call": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/call/-/call-5.0.1.tgz", - "integrity": "sha512-ollfFPSshiuYLp7AsrmpkQJ/PxCi6AzV81rCjBwWhyF2QGyUY/vPDMzoh4aUcWyucheRglG2LaS5qkIEfLRh6A==", - "requires": { - "boom": "7.x.x", - "hoek": "5.x.x" - }, - "dependencies": { - "boom": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-7.2.0.tgz", - "integrity": "sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY=", - "requires": { - "hoek": "5.x.x" - } - }, - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - } - } - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "^0.2.0" - } - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - } - } - }, - "capture-stack-trace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", - "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "catbox": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/catbox/-/catbox-10.0.3.tgz", - "integrity": "sha512-qwus6RnVctHXYwfxvvDwvlMWHwCjQdIpQQbtyHnRF0JpwmxbQJ/UIZi9y8O6DpphKCdfO9gpxgb2ne9ZDx39BQ==", - "requires": { - "boom": "7.x.x", - "hoek": "5.x.x", - "joi": "13.x.x" - }, - "dependencies": { - "boom": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-7.2.0.tgz", - "integrity": "sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY=", - "requires": { - "hoek": "5.x.x" - } - }, - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - }, - "joi": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz", - "integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==", - "requires": { - "hoek": "5.x.x", - "isemail": "3.x.x", - "topo": "3.x.x" - } - } - } - }, - "catbox-memory": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/catbox-memory/-/catbox-memory-3.1.2.tgz", - "integrity": "sha512-lhWtutLVhsq3Mucxk2McxBPPibJ34WcHuWFz3xqub9u9Ve/IQYpZv3ijLhQXfQped9DXozURiaq9O3aZpP91eg==", - "requires": { - "big-time": "2.x.x", - "boom": "7.x.x", - "hoek": "5.x.x" - }, - "dependencies": { - "boom": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-7.2.0.tgz", - "integrity": "sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY=", - "requires": { - "hoek": "5.x.x" - } - }, - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - } - } - }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - } - }, - "chai": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", - "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", - "dev": true, - "requires": { - "assertion-error": "^1.0.1", - "deep-eql": "^0.1.3", - "type-detect": "^1.0.0" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "chokidar": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", - "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.0", - "braces": "^2.3.0", - "fsevents": "^1.2.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "lodash.debounce": "^4.0.8", - "normalize-path": "^2.1.1", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0", - "upath": "^1.0.5" - } - }, - "ci-info": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", - "integrity": "sha512-SK/846h/Rcy8q9Z9CAwGBLfCJ6EkjJWdpelWDufQpqVDYq2Wnnv8zlSO6AMQap02jvhVruKKpEtQOufo3pFhLg==", - "dev": true - }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", - "dev": true - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "dev": true, - "requires": { - "restore-cursor": "^1.0.1" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dev": true, - "optional": true, - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true, - "optional": true - } - } - }, - "clone": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/clone/-/clone-0.1.19.tgz", - "integrity": "sha1-YT+2hjmyaklKxTJT4Vsaa9iK2oU=" - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "coffeescript": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.10.0.tgz", - "integrity": "sha1-56qDAZF+9iGzXYo580jc3R234z4=", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", - "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", - "dev": true, - "requires": { - "color-name": "1.1.1" - } - }, - "color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", - "dev": true - }, - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" - }, - "compare-func": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.1.tgz", - "integrity": "sha1-hZTw/98j/AIfpRdheQvpvCctI68=", - "dev": true, - "requires": { - "array-ify": "^1.0.0", - "dot-prop": "^2.0.0" - } - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "configstore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", - "dev": true, - "requires": { - "dot-prop": "^4.1.0", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" - }, - "dependencies": { - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - } - } - }, - "content": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/content/-/content-4.0.5.tgz", - "integrity": "sha512-wDP6CTWDpwCf791fNxlCCkZGRkrNzSEU/8ju9Hnr3Uc5mF/gFR5W+fcoGm6zUSlVPdSXYn5pCbySADKj7YM4Cg==", - "requires": { - "boom": "7.x.x" - }, - "dependencies": { - "boom": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-7.2.0.tgz", - "integrity": "sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY=", - "requires": { - "hoek": "5.x.x" - } - }, - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - } - } - }, - "conventional-changelog": { - "version": "1.1.24", - "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-1.1.24.tgz", - "integrity": "sha512-2WcSUst4Y3Z4hHvoMTWXMJr/DmgVdLiMOVY1Kak2LfFz+GIz2KDp5naqbFesYbfXPmaZ5p491dO0FWZIJoJw1Q==", - "dev": true, - "requires": { - "conventional-changelog-angular": "^1.6.6", - "conventional-changelog-atom": "^0.2.8", - "conventional-changelog-codemirror": "^0.3.8", - "conventional-changelog-core": "^2.0.11", - "conventional-changelog-ember": "^0.3.12", - "conventional-changelog-eslint": "^1.0.9", - "conventional-changelog-express": "^0.3.6", - "conventional-changelog-jquery": "^0.1.0", - "conventional-changelog-jscs": "^0.1.0", - "conventional-changelog-jshint": "^0.3.8", - "conventional-changelog-preset-loader": "^1.1.8" - } - }, - "conventional-changelog-angular": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-1.6.6.tgz", - "integrity": "sha512-suQnFSqCxRwyBxY68pYTsFkG0taIdinHLNEAX5ivtw8bCRnIgnpvcHmlR/yjUyZIrNPYAoXlY1WiEKWgSE4BNg==", - "dev": true, - "requires": { - "compare-func": "^1.3.1", - "q": "^1.5.1" - } - }, - "conventional-changelog-atom": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/conventional-changelog-atom/-/conventional-changelog-atom-0.2.8.tgz", - "integrity": "sha512-8pPZqhMbrnltNBizjoDCb/Sz85KyUXNDQxuAEYAU5V/eHn0okMBVjqc8aHWYpHrytyZWvMGbayOlDv7i8kEf6g==", - "dev": true, - "requires": { - "q": "^1.5.1" - } - }, - "conventional-changelog-codemirror": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/conventional-changelog-codemirror/-/conventional-changelog-codemirror-0.3.8.tgz", - "integrity": "sha512-3HFZKtBXTaUCHvz7ai6nk2+psRIkldDoNzCsom0egDtVmPsvvHZkzjynhdQyULfacRSsBTaiQ0ol6nBOL4dDiQ==", - "dev": true, - "requires": { - "q": "^1.5.1" - } - }, - "conventional-changelog-core": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-2.0.11.tgz", - "integrity": "sha512-HvTE6RlqeEZ/NFPtQeFLsIDOLrGP3bXYr7lFLMhCVsbduF1MXIe8OODkwMFyo1i9ku9NWBwVnVn0jDmIFXjDRg==", - "dev": true, - "requires": { - "conventional-changelog-writer": "^3.0.9", - "conventional-commits-parser": "^2.1.7", - "dateformat": "^3.0.0", - "get-pkg-repo": "^1.0.0", - "git-raw-commits": "^1.3.6", - "git-remote-origin-url": "^2.0.0", - "git-semver-tags": "^1.3.6", - "lodash": "^4.2.1", - "normalize-package-data": "^2.3.5", - "q": "^1.5.1", - "read-pkg": "^1.1.0", - "read-pkg-up": "^1.0.1", - "through2": "^2.0.0" - }, - "dependencies": { - "dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", - "dev": true - } - } - }, - "conventional-changelog-ember": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/conventional-changelog-ember/-/conventional-changelog-ember-0.3.12.tgz", - "integrity": "sha512-mmJzA7uzbrOqeF89dMMi6z17O07ORTXlTMArnLG9ZTX4oLaKNolUlxFUFlFm9JUoVWajVpaHQWjxH1EOQ+ARoQ==", - "dev": true, - "requires": { - "q": "^1.5.1" - } - }, - "conventional-changelog-eslint": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/conventional-changelog-eslint/-/conventional-changelog-eslint-1.0.9.tgz", - "integrity": "sha512-h87nfVh2fdk9fJIvz26wCBsbDC/KxqCc5wSlNMZbXcARtbgNbNDIF7Y7ctokFdnxkzVdaHsbINkh548T9eBA7Q==", - "dev": true, - "requires": { - "q": "^1.5.1" - } - }, - "conventional-changelog-express": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/conventional-changelog-express/-/conventional-changelog-express-0.3.6.tgz", - "integrity": "sha512-3iWVtBJZ9RnRnZveNDzOD8QRn6g6vUif0qVTWWyi5nUIAbuN1FfPVyKdAlJJfp5Im+dE8Kiy/d2SpaX/0X678Q==", - "dev": true, - "requires": { - "q": "^1.5.1" - } - }, - "conventional-changelog-jquery": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-jquery/-/conventional-changelog-jquery-0.1.0.tgz", - "integrity": "sha1-Agg5cWLjhGmG5xJztsecW1+A9RA=", - "dev": true, - "requires": { - "q": "^1.4.1" - } - }, - "conventional-changelog-jscs": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-jscs/-/conventional-changelog-jscs-0.1.0.tgz", - "integrity": "sha1-BHnrRDzH1yxYvwvPDvHURKkvDlw=", - "dev": true, - "requires": { - "q": "^1.4.1" - } - }, - "conventional-changelog-jshint": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/conventional-changelog-jshint/-/conventional-changelog-jshint-0.3.8.tgz", - "integrity": "sha512-hn9QU4ZI/5V50wKPJNPGT4gEWgiBFpV6adieILW4MaUFynuDYOvQ71EMSj3EznJyKi/KzuXpc9dGmX8njZMjig==", - "dev": true, - "requires": { - "compare-func": "^1.3.1", - "q": "^1.5.1" - } - }, - "conventional-changelog-preset-loader": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-1.1.8.tgz", - "integrity": "sha512-MkksM4G4YdrMlT2MbTsV2F6LXu/hZR0Tc/yenRrDIKRwBl/SP7ER4ZDlglqJsCzLJi4UonBc52Bkm5hzrOVCcw==", - "dev": true - }, - "conventional-changelog-writer": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-3.0.9.tgz", - "integrity": "sha512-n9KbsxlJxRQsUnK6wIBRnARacvNnN4C/nxnxCkH+B/R1JS2Fa+DiP1dU4I59mEDEjgnFaN2+9wr1P1s7GYB5/Q==", - "dev": true, - "requires": { - "compare-func": "^1.3.1", - "conventional-commits-filter": "^1.1.6", - "dateformat": "^3.0.0", - "handlebars": "^4.0.2", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.2.1", - "meow": "^4.0.0", - "semver": "^5.5.0", - "split": "^1.0.0", - "through2": "^2.0.0" - }, - "dependencies": { - "camelcase-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", - "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", - "dev": true, - "requires": { - "camelcase": "^4.1.0", - "map-obj": "^2.0.0", - "quick-lru": "^1.0.0" - } - }, - "dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", - "dev": true - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", - "dev": true - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "map-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", - "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", - "dev": true - }, - "meow": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", - "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", - "dev": true, - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist": "^1.1.3", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "redent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", - "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", - "dev": true, - "requires": { - "indent-string": "^3.0.0", - "strip-indent": "^2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", - "dev": true - }, - "trim-newlines": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", - "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", - "dev": true - } - } - }, - "conventional-commits-filter": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-1.1.6.tgz", - "integrity": "sha512-KcDgtCRKJCQhyk6VLT7zR+ZOyCnerfemE/CsR3iQpzRRFbLEs0Y6rwk3mpDvtOh04X223z+1xyJ582Stfct/0Q==", - "dev": true, - "requires": { - "is-subset": "^0.1.1", - "modify-values": "^1.0.0" - } - }, - "conventional-commits-parser": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-2.1.7.tgz", - "integrity": "sha512-BoMaddIEJ6B4QVMSDu9IkVImlGOSGA1I2BQyOZHeLQ6qVOJLcLKn97+fL6dGbzWEiqDzfH4OkcveULmeq2MHFQ==", - "dev": true, - "requires": { - "JSONStream": "^1.0.4", - "is-text-path": "^1.0.0", - "lodash": "^4.2.1", - "meow": "^4.0.0", - "split2": "^2.0.0", - "through2": "^2.0.0", - "trim-off-newlines": "^1.0.0" - }, - "dependencies": { - "camelcase-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", - "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", - "dev": true, - "requires": { - "camelcase": "^4.1.0", - "map-obj": "^2.0.0", - "quick-lru": "^1.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", - "dev": true - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "map-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", - "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", - "dev": true - }, - "meow": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", - "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", - "dev": true, - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist": "^1.1.3", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "redent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", - "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", - "dev": true, - "requires": { - "indent-string": "^3.0.0", - "strip-indent": "^2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", - "dev": true - }, - "trim-newlines": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", - "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", - "dev": true - } - } - }, - "convict": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/convict/-/convict-4.0.2.tgz", - "integrity": "sha1-oVFl1DwhHRJfV8ZNKLxWHcKdNb4=", - "requires": { - "depd": "1.1.1", - "json5": "0.5.1", - "lodash.clonedeep": "4.5.0", - "moment": "2.19.3", - "validator": "7.2.0", - "varify": "0.2.0", - "yargs-parser": "7.0.0" - } - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "dev": true, - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "5.x.x" - } - }, - "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", - "dev": true - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "requires": { - "array-find-index": "^1.0.1" - } - }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "dev": true, - "requires": { - "es5-ext": "^0.10.9" - } - }, - "dargs": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", - "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "date-time": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/date-time/-/date-time-1.1.0.tgz", - "integrity": "sha1-GIdtC9pMGf5w3Tv0sDTygbEqQLY=", - "dev": true, - "requires": { - "time-zone": "^0.1.0" - } - }, - "dateformat": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", - "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1", - "meow": "^3.3.0" - } - }, - "dbug": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/dbug/-/dbug-0.4.2.tgz", - "integrity": "sha1-MrSzEF6IYQQ6b5rHVdgOVC02WzE=" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decamelize-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", - "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", - "dev": true, - "requires": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - } - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", - "dev": true, - "requires": { - "type-detect": "0.1.1" - }, - "dependencies": { - "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", - "dev": true - } - } - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", - "dev": true, - "requires": { - "globby": "^5.0.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "rimraf": "^2.2.8" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" - } - }, - "dot-prop": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-2.4.0.tgz", - "integrity": "sha1-hI4o9/HVB0DGdHqzywdnBGK2+Jw=", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, - "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", - "dev": true - }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "~0.1.0" - } - }, - "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "requires": { - "iconv-lite": "~0.4.13" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es5-ext": { - "version": "0.10.45", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.45.tgz", - "integrity": "sha512-FkfM6Vxxfmztilbxxz5UKSD4ICMf5tSpRFtDNtkAhOxZ0EKtX6qwmXNyH/sFyIbX2P/nU5AMiA9jilWsUGJzCQ==", - "dev": true, - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "1" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-set": "~0.1.5", - "es6-symbol": "~3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-symbol": "3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.14", - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "escope": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", - "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", - "dev": true, - "requires": { - "es6-map": "^0.1.3", - "es6-weak-map": "^2.0.1", - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-2.13.1.tgz", - "integrity": "sha1-5MyPoPAJ+4KaquI4VaKTYL4fbBE=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "concat-stream": "^1.4.6", - "debug": "^2.1.1", - "doctrine": "^1.2.2", - "es6-map": "^0.1.3", - "escope": "^3.6.0", - "espree": "^3.1.6", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "file-entry-cache": "^1.1.1", - "glob": "^7.0.3", - "globals": "^9.2.0", - "ignore": "^3.1.2", - "imurmurhash": "^0.1.4", - "inquirer": "^0.12.0", - "is-my-json-valid": "^2.10.0", - "is-resolvable": "^1.0.0", - "js-yaml": "^3.5.1", - "json-stable-stringify": "^1.0.0", - "levn": "^0.3.0", - "lodash": "^4.0.0", - "mkdirp": "^0.5.0", - "optionator": "^0.8.1", - "path-is-absolute": "^1.0.0", - "path-is-inside": "^1.0.1", - "pluralize": "^1.2.1", - "progress": "^1.1.8", - "require-uncached": "^1.0.2", - "shelljs": "^0.6.0", - "strip-json-comments": "~1.0.1", - "table": "^3.7.8", - "text-table": "~0.2.0", - "user-home": "^2.0.0" - }, - "dependencies": { - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "eslint-plugin-fxa": { - "version": "git://github.com/mozilla/eslint-plugin-fxa.git#41504c9dd30e8b52900c15b524946aa0428aef95", - "from": "eslint-plugin-fxa@git://github.com/mozilla/eslint-plugin-fxa.git#41504c9dd30e8b52900c15b524946aa0428aef95", - "dev": true - }, - "espree": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", - "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", - "dev": true, - "requires": { - "acorn": "^5.5.0", - "acorn-jsx": "^3.0.0" - } - }, - "esprima": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.0.0.tgz", - "integrity": "sha1-U88kes2ncxPlUcOqLnM0LT+099k=", - "optional": true - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "dev": true, - "requires": { - "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "event-stream": { - "version": "3.3.4", - "resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", - "dev": true, - "requires": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - }, - "dependencies": { - "split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", - "dev": true, - "requires": { - "through": "2" - } - } - } - }, - "eventemitter2": { - "version": "0.4.14", - "resolved": "http://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", - "dev": true - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "dev": true - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fetch": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/fetch/-/fetch-0.3.6.tgz", - "integrity": "sha1-N1Q3GMIqisA8fHscUTe7N/Y9g9g=", - "requires": { - "encoding": "*" - } - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "file-entry-cache": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-1.3.1.tgz", - "integrity": "sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g=", - "dev": true, - "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" - } - }, - "fill-keys": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", - "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", - "dev": true, - "requires": { - "is-object": "~1.0.1", - "merge-descriptors": "~1.0.0" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "findup-sync": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", - "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", - "dev": true, - "requires": { - "glob": "~5.0.0" - } - }, - "flat-cache": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", - "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", - "dev": true, - "requires": { - "circular-json": "^0.3.1", - "del": "^2.0.2", - "graceful-fs": "^4.1.2", - "write": "^0.2.1" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "1.0.6", - "mime-types": "^2.1.12" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": false, - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "aproba": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "resolved": false, - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": false, - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": false, - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "resolved": false, - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "resolved": false, - "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "resolved": false, - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "resolved": false, - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "resolved": false, - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": false, - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "resolved": false, - "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "^2.1.0" - } - }, - "ignore-walk": { - "version": "3.0.1", - "resolved": false, - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": false, - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": false, - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "ini": { - "version": "1.3.5", - "resolved": false, - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": false, - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": false, - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "minipass": { - "version": "2.2.4", - "resolved": false, - "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.1", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": false, - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "resolved": false, - "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", - "dev": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "resolved": false, - "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.1.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "resolved": false, - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.3", - "resolved": false, - "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "resolved": false, - "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": false, - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": false, - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "resolved": false, - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "resolved": false, - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "resolved": false, - "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.5.1", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": false, - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": false, - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, - "optional": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": false, - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": false, - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "resolved": false, - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "resolved": false, - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": false, - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": false, - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "resolved": false, - "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==", - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "resolved": false, - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "yallist": { - "version": "3.0.2", - "resolved": false, - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true - } - } - }, - "fxa-conventional-changelog": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fxa-conventional-changelog/-/fxa-conventional-changelog-1.1.0.tgz", - "integrity": "sha1-u4al+cnBSjP0FieAQ6BqKOIVz0w=", - "dev": true, - "requires": { - "compare-func": "1.3.1", - "lodash.map": "3.1.4", - "semver": "5.0.1" - }, - "dependencies": { - "semver": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.1.tgz", - "integrity": "sha1-n7P0AE+QDYPEeWj+QvdYPgWDLMk=", - "dev": true - } - } - }, - "fxa-jwtool": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/fxa-jwtool/-/fxa-jwtool-0.7.2.tgz", - "integrity": "sha1-Mu61wC4CWCjrIPUIFiyJ+vSjS/s=", - "requires": { - "bluebird": "3.0.5", - "fetch": "0.3.6", - "pem-jwk": "1.5.1" - }, - "dependencies": { - "bluebird": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.0.5.tgz", - "integrity": "sha1-L/nQfJs+2ynW0oD+B1KDZefs05I=" - } - } - }, - "fxa-notifier-aws": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fxa-notifier-aws/-/fxa-notifier-aws-1.0.0.tgz", - "integrity": "sha1-xPUOSAHGu9QStX6rnxHUN4A2PcE=", - "requires": { - "aws-sdk": "2.0.23" - } - }, - "fxa-shared": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/fxa-shared/-/fxa-shared-1.0.13.tgz", - "integrity": "sha512-jy9FehibrocELTpXPEfr6cWMrac+N//7CBBPACAK17ZHuN/ZP5iJuIG3nB18ZIZKBSmJH0TgO8C6p3XocSx2mA==", - "requires": { - "accept-language": "2.0.17", - "moment": "2.20.1" - }, - "dependencies": { - "moment": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", - "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==" - } - } - }, - "generate-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", - "dev": true - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, - "requires": { - "is-property": "^1.0.0" - } - }, - "get-pkg-repo": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz", - "integrity": "sha1-xztInAbYDMVTbCyFP54FIyBWly0=", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "meow": "^3.3.0", - "normalize-package-data": "^2.3.0", - "parse-github-repo-url": "^1.3.0", - "through2": "^2.0.0" - } - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getobject": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", - "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "git-raw-commits": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-1.3.6.tgz", - "integrity": "sha512-svsK26tQ8vEKnMshTDatSIQSMDdz8CxIIqKsvPqbtV23Etmw6VNaFAitu8zwZ0VrOne7FztwPyRLxK7/DIUTQg==", - "dev": true, - "requires": { - "dargs": "^4.0.1", - "lodash.template": "^4.0.2", - "meow": "^4.0.0", - "split2": "^2.0.0", - "through2": "^2.0.0" - }, - "dependencies": { - "camelcase-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", - "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", - "dev": true, - "requires": { - "camelcase": "^4.1.0", - "map-obj": "^2.0.0", - "quick-lru": "^1.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", - "dev": true - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "map-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", - "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", - "dev": true - }, - "meow": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", - "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", - "dev": true, - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist": "^1.1.3", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "redent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", - "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", - "dev": true, - "requires": { - "indent-string": "^3.0.0", - "strip-indent": "^2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", - "dev": true - }, - "trim-newlines": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", - "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", - "dev": true - } - } - }, - "git-remote-origin-url": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", - "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", - "dev": true, - "requires": { - "gitconfiglocal": "^1.0.0", - "pify": "^2.3.0" - } - }, - "git-semver-tags": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-1.3.6.tgz", - "integrity": "sha512-2jHlJnln4D/ECk9FxGEBh3k44wgYdWjWDtMmJPaecjoRmxKo3Y1Lh8GMYuOPu04CHw86NTAODchYjC5pnpMQig==", - "dev": true, - "requires": { - "meow": "^4.0.0", - "semver": "^5.5.0" - }, - "dependencies": { - "camelcase-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", - "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", - "dev": true, - "requires": { - "camelcase": "^4.1.0", - "map-obj": "^2.0.0", - "quick-lru": "^1.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", - "dev": true - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "map-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", - "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", - "dev": true - }, - "meow": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", - "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", - "dev": true, - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist": "^1.1.3", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "redent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", - "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", - "dev": true, - "requires": { - "indent-string": "^3.0.0", - "strip-indent": "^2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", - "dev": true - }, - "trim-newlines": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", - "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", - "dev": true - } - } - }, - "gitconfiglocal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", - "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", - "dev": true, - "requires": { - "ini": "^1.3.2" - } - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", - "dev": true, - "requires": { - "ini": "^1.3.4" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "globby": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "dev": true, - "requires": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "grunt": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.0.3.tgz", - "integrity": "sha512-/JzmZNPfKorlCrrmxWqQO4JVodO+DVd5XX4DkocL/1WlLlKVLE9+SdEIempOAxDhWPysLle6afvn/hg7Ck2k9g==", - "dev": true, - "requires": { - "coffeescript": "~1.10.0", - "dateformat": "~1.0.12", - "eventemitter2": "~0.4.13", - "exit": "~0.1.1", - "findup-sync": "~0.3.0", - "glob": "~7.0.0", - "grunt-cli": "~1.2.0", - "grunt-known-options": "~1.1.0", - "grunt-legacy-log": "~2.0.0", - "grunt-legacy-util": "~1.1.1", - "iconv-lite": "~0.4.13", - "js-yaml": "~3.5.2", - "minimatch": "~3.0.2", - "mkdirp": "~0.5.1", - "nopt": "~3.0.6", - "path-is-absolute": "~1.0.0", - "rimraf": "~2.6.2" - }, - "dependencies": { - "glob": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", - "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.2", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "grunt-cli": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz", - "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=", - "dev": true, - "requires": { - "findup-sync": "~0.3.0", - "grunt-known-options": "~1.1.0", - "nopt": "~3.0.6", - "resolve": "~1.1.0" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, - "requires": { - "glob": "^7.0.5" - } - } - } - }, - "grunt-bump": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/grunt-bump/-/grunt-bump-0.8.0.tgz", - "integrity": "sha1-0//gzzzws44JYHt4U49CpTHq/lU=", - "dev": true, - "requires": { - "semver": "^5.1.0" - } - }, - "grunt-conventional-changelog": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/grunt-conventional-changelog/-/grunt-conventional-changelog-6.1.0.tgz", - "integrity": "sha1-mL1b37Kw3mMWwFx8b6ykziTdFDU=", - "dev": true, - "requires": { - "chalk": "^1.1.0", - "concat-stream": "^1.5.0", - "conventional-changelog": "^1.1.0", - "plur": "^2.0.0", - "q": "^1.4.1" - } - }, - "grunt-copyright": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/grunt-copyright/-/grunt-copyright-0.3.0.tgz", - "integrity": "sha1-5uv5UV8sRILqMV1YZYKHjSMUY5g=", - "dev": true - }, - "grunt-eslint": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/grunt-eslint/-/grunt-eslint-18.0.0.tgz", - "integrity": "sha1-QOAe2Vx++J4PBdygHy3bM8JtEjg=", - "dev": true, - "requires": { - "chalk": "^1.0.0", - "eslint": "^2.0.0" - } - }, - "grunt-known-options": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.1.tgz", - "integrity": "sha512-cHwsLqoighpu7TuYj5RonnEuxGVFnztcUqTqp5rXFGYL4OuPFofwC4Ycg7n9fYwvK6F5WbYgeVOwph9Crs2fsQ==", - "dev": true - }, - "grunt-legacy-log": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-2.0.0.tgz", - "integrity": "sha512-1m3+5QvDYfR1ltr8hjiaiNjddxGdQWcH0rw1iKKiQnF0+xtgTazirSTGu68RchPyh1OBng1bBUjLmX8q9NpoCw==", - "dev": true, - "requires": { - "colors": "~1.1.2", - "grunt-legacy-log-utils": "~2.0.0", - "hooker": "~0.2.3", - "lodash": "~4.17.5" - } - }, - "grunt-legacy-log-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.0.1.tgz", - "integrity": "sha512-o7uHyO/J+i2tXG8r2bZNlVk20vlIFJ9IEYyHMCQGfWYru8Jv3wTqKZzvV30YW9rWEjq0eP3cflQ1qWojIe9VFA==", - "dev": true, - "requires": { - "chalk": "~2.4.1", - "lodash": "~4.17.10" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "grunt-legacy-util": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-1.1.1.tgz", - "integrity": "sha512-9zyA29w/fBe6BIfjGENndwoe1Uy31BIXxTH3s8mga0Z5Bz2Sp4UCjkeyv2tI449ymkx3x26B+46FV4fXEddl5A==", - "dev": true, - "requires": { - "async": "~1.5.2", - "exit": "~0.1.1", - "getobject": "~0.1.0", - "hooker": "~0.2.3", - "lodash": "~4.17.10", - "underscore.string": "~3.3.4", - "which": "~1.3.0" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "grunt-nodemon": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/grunt-nodemon/-/grunt-nodemon-0.4.2.tgz", - "integrity": "sha1-rrwSq2bXtbOwVM3Yxfgi7BpCPvk=", - "dev": true, - "requires": { - "nodemon": "^1.7.1" - } - }, - "handlebars": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", - "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", - "dev": true, - "requires": { - "async": "^1.4.0", - "optimist": "^0.6.1", - "source-map": "^0.4.4", - "uglify-js": "^2.6" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - } - } - }, - "hapi": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/hapi/-/hapi-17.6.0.tgz", - "integrity": "sha512-GSHjE1hJExluAukrT/QuYSk96irmbYBDd3wOgywiHsPoR2QeKgDnIttD+dB6NbADEmSdb9MS5gTUIVq0uHTdkA==", - "requires": { - "accept": "3.x.x", - "ammo": "3.x.x", - "boom": "7.x.x", - "bounce": "1.x.x", - "call": "5.x.x", - "catbox": "10.x.x", - "catbox-memory": "3.x.x", - "heavy": "6.x.x", - "hoek": "5.x.x", - "joi": "13.x.x", - "mimos": "4.x.x", - "podium": "3.x.x", - "shot": "4.x.x", - "statehood": "6.x.x", - "subtext": "6.x.x", - "teamwork": "3.x.x", - "topo": "3.x.x" - }, - "dependencies": { - "boom": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-7.2.0.tgz", - "integrity": "sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY=", - "requires": { - "hoek": "5.x.x" - } - }, - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - }, - "joi": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz", - "integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==", - "requires": { - "hoek": "5.x.x", - "isemail": "3.x.x", - "topo": "3.x.x" - } - } - } - }, - "hapi-hpkp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hapi-hpkp/-/hapi-hpkp-2.0.0.tgz", - "integrity": "sha512-Kko4pgRWp3Q5VAZySU7IOYv1RJ3s8BXtmYtV44BaWINWQgt5vsDuKiKIZBzO2X8hphCkGw0tdkZFu1yrhIUscg==", - "requires": { - "joi": "13.3.0" - }, - "dependencies": { - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - }, - "joi": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-13.3.0.tgz", - "integrity": "sha512-iF6jEYVfBIoYXztYymia1JfuoVbxBNuOcwdbsdoGin9/jjhBLhonKmfTQOvePss8r8v4tU4JOcNmYPHZzKEFag==", - "requires": { - "hoek": "5.x.x", - "isemail": "3.x.x", - "topo": "3.x.x" - } - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "^5.1.0", - "har-schema": "^2.0.0" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "requires": { - "boom": "4.x.x", - "cryptiles": "3.x.x", - "hoek": "4.x.x", - "sntp": "2.x.x" - }, - "dependencies": { - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.x.x" - } - } - } - }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true - }, - "heavy": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/heavy/-/heavy-6.1.0.tgz", - "integrity": "sha512-TKS9DC9NOTGulHQI31Lx+bmeWmNOstbJbGMiN3pX6bF+Zc2GKSpbbym4oasNnB6yPGkqJ9TQXXYDGohqNSJRxA==", - "requires": { - "boom": "7.x.x", - "hoek": "5.x.x", - "joi": "13.x.x" - }, - "dependencies": { - "boom": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-7.2.0.tgz", - "integrity": "sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY=", - "requires": { - "hoek": "5.x.x" - } - }, - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - }, - "joi": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz", - "integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==", - "requires": { - "hoek": "5.x.x", - "isemail": "3.x.x", - "topo": "3.x.x" - } - } - } - }, - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" - }, - "hooker": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", - "dev": true - }, - "hosted-git-info": { - "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==", - "dev": true - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-proxy-agent": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-0.3.6.tgz", - "integrity": "sha1-cT+jjl01P1DrFKNC/r4pAz7RYZs=", - "requires": { - "agent-base": "~1.0.1", - "debug": "2", - "extend": "3" - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", - "dev": true - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true - }, - "inquirer": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", - "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", - "dev": true, - "requires": { - "ansi-escapes": "^1.1.0", - "ansi-regex": "^2.0.0", - "chalk": "^1.0.0", - "cli-cursor": "^1.0.1", - "cli-width": "^2.0.0", - "figures": "^1.3.5", - "lodash": "^4.3.0", - "readline2": "^1.0.1", - "run-async": "^0.1.0", - "rx-lite": "^3.1.2", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.0", - "through": "^2.3.6" - } - }, - "insist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/insist/-/insist-1.0.1.tgz", - "integrity": "sha1-ITdKECnUoTpidEEV38LTMTQYldQ=", - "dev": true, - "requires": { - "esprima": "^4.0.0", - "stack-trace": "^0.0.10" - }, - "dependencies": { - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - } - } - }, - "intel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/intel/-/intel-1.2.0.tgz", - "integrity": "sha1-EdEUfraz9Fgr31M3s31UFYTp5B4=", - "requires": { - "chalk": "^1.1.0", - "dbug": "~0.4.2", - "stack-trace": "~0.0.9", - "strftime": "~0.10.0", - "symbol": "~0.3.1", - "utcstring": "~0.1.0" - } - }, - "iron": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/iron/-/iron-5.0.4.tgz", - "integrity": "sha512-7iQ5/xFMIYaNt9g2oiNiWdhrOTdRUMFaWENUd0KghxwPUhrIH8DUY8FEyLNTTzf75jaII+jMexLdY/2HfV61RQ==", - "requires": { - "boom": "7.x.x", - "cryptiles": "4.x.x", - "hoek": "5.x.x" - }, - "dependencies": { - "boom": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-7.2.0.tgz", - "integrity": "sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY=", - "requires": { - "hoek": "5.x.x" - } - }, - "cryptiles": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-4.1.2.tgz", - "integrity": "sha512-U2ALcoAHvA1oO2xOreyHvtkQ+IELqDG2WVWRI1GH/XEmmfGIOalnM5MU5Dd2ITyWfr3m6kNqXiy8XuYyd4wKJw==", - "requires": { - "boom": "7.x.x" - } - }, - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - } - } - }, - "irregular-plurals": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-1.4.0.tgz", - "integrity": "sha1-LKmwM2UREYVUEvFr5dd8YqRYp2Y=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "dev": true, - "requires": { - "builtin-modules": "^1.0.0" - } - }, - "is-ci": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", - "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", - "dev": true, - "requires": { - "ci-info": "^1.0.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", - "dev": true, - "requires": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" - } - }, - "is-my-ip-valid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", - "dev": true - }, - "is-my-json-valid": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", - "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", - "dev": true, - "requires": { - "generate-function": "^2.0.0", - "generate-object-property": "^1.1.0", - "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^4.0.0", - "xtend": "^4.0.0" - } - }, - "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", - "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", - "dev": true - }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", - "dev": true, - "requires": { - "is-path-inside": "^1.0.0" - } - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", - "dev": true - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-subset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", - "dev": true - }, - "is-text-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", - "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", - "dev": true, - "requires": { - "text-extensions": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isemail": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.1.4.tgz", - "integrity": "sha512-yE/W5osEWuAGSLVixV9pAexhkbZzglmuhO2CxdHu7IBh7uzuZogQ4bk0lE26HoZ6HD4ZYfKRKilkNuCnuJIBJw==", - "requires": { - "punycode": "2.x.x" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "istanbul-lib-coverage": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", - "integrity": "sha512-nPvSZsVlbG9aLhZYaC3Oi1gT/tpyo3Yt5fNyf6NmcKIayz4VV/txxJFFKAK/gU4dcNn8ehsanBbVHVl0+amOLA==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.0.0.tgz", - "integrity": "sha512-eQY9vN9elYjdgN9Iv6NS/00bptm02EBBk70lRMaVjeA6QYocQgenVrSgC28TJurdnZa80AGO3ASdFN+w/njGiQ==", - "dev": true, - "requires": { - "@babel/generator": "^7.0.0", - "@babel/parser": "^7.0.0", - "@babel/template": "^7.0.0", - "@babel/traverse": "^7.0.0", - "@babel/types": "^7.0.0", - "istanbul-lib-coverage": "^2.0.1", - "semver": "^5.5.0" - } - }, - "joi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-14.0.0.tgz", - "integrity": "sha512-jEu+bPFcsgdPr85hVyjb5D5grxLEZniT6AB1vjewrRDbuYxe2r5quyxs3E32dF8fCXcaJnlRSy4jehSpDuNMNg==", - "requires": { - "hoek": "5.x.x", - "isemail": "3.x.x", - "topo": "3.x.x" - }, - "dependencies": { - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - } - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.5.5.tgz", - "integrity": "sha1-A3fDgBfKvHMisNH7zSWkkWQfL74=", - "dev": true, - "requires": { - "argparse": "^1.0.2", - "esprima": "^2.6.0" - }, - "dependencies": { - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - } - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "jsesc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz", - "integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", - "dev": true - }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "just-extend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-3.0.0.tgz", - "integrity": "sha512-Fu3T6pKBuxjWT/p4DkqGHFRsysc8OauWr4ZRTY9dIx07Y9O0RkoR5jcv28aeD1vuAwhm3nLkDurwLXoALp4DpQ==", - "dev": true - }, - "keypair": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/keypair/-/keypair-1.0.1.tgz", - "integrity": "sha1-dgNxknCvtlZO04oiCHoG/Jqk6hs=" - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - }, - "latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", - "dev": true, - "requires": { - "package-json": "^4.0.0" - } - }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true, - "optional": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "load-grunt-tasks": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/load-grunt-tasks/-/load-grunt-tasks-3.5.2.tgz", - "integrity": "sha1-ByhWEYD9IP+KaSdQWFL8WKrqDIg=", - "dev": true, - "requires": { - "arrify": "^1.0.0", - "multimatch": "^2.0.0", - "pkg-up": "^1.0.0", - "resolve-pkg": "^0.1.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "dependencies": { - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - } - } - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", - "dev": true - }, - "lodash._arraymap": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._arraymap/-/lodash._arraymap-3.0.0.tgz", - "integrity": "sha1-Go/Q9MDfS2HeoHbXF83Jfwo8PmY=", - "dev": true - }, - "lodash._basecallback": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/lodash._basecallback/-/lodash._basecallback-3.3.1.tgz", - "integrity": "sha1-t7K7Q9whYEJKIczybFfkQ3cqjic=", - "dev": true, - "requires": { - "lodash._baseisequal": "^3.0.0", - "lodash._bindcallback": "^3.0.0", - "lodash.isarray": "^3.0.0", - "lodash.pairs": "^3.0.0" - } - }, - "lodash._baseeach": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash._baseeach/-/lodash._baseeach-3.0.4.tgz", - "integrity": "sha1-z4cGVyyhROjZ11InyZDamC+TKvM=", - "dev": true, - "requires": { - "lodash.keys": "^3.0.0" - } - }, - "lodash._baseisequal": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz", - "integrity": "sha1-2AJfdjOdKTQnZ9zIh85cuVpbUfE=", - "dev": true, - "requires": { - "lodash.isarray": "^3.0.0", - "lodash.istypedarray": "^3.0.0", - "lodash.keys": "^3.0.0" - } - }, - "lodash._bindcallback": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", - "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", - "dev": true - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", - "dev": true - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" - }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true - }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, - "lodash.istypedarray": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz", - "integrity": "sha1-yaR3SYYHUB2OhJTSg7h8OSgc72I=", - "dev": true - }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, - "lodash.map": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-3.1.4.tgz", - "integrity": "sha1-tIOs0beGxce0ksSV97UmYim8AMI=", - "dev": true, - "requires": { - "lodash._arraymap": "^3.0.0", - "lodash._basecallback": "^3.0.0", - "lodash._baseeach": "^3.0.0", - "lodash.isarray": "^3.0.0", - "lodash.keys": "^3.0.0" - } - }, - "lodash.pairs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash.pairs/-/lodash.pairs-3.0.1.tgz", - "integrity": "sha1-u+CNV4bu6qCaFckevw3LfSvjJqk=", - "dev": true, - "requires": { - "lodash.keys": "^3.0.0" - } - }, - "lodash.template": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", - "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", - "dev": true, - "requires": { - "lodash._reinterpolate": "~3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", - "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", - "dev": true, - "requires": { - "lodash._reinterpolate": "~3.0.0" - } - }, - "lolex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.0.0.tgz", - "integrity": "sha512-hcnW80h3j2lbUfFdMArd5UPA/vxZJ+G8vobd+wg3nVEQA0EigStbYcrG030FJxL6xiDDPEkoMatV9xIh5OecQQ==", - "dev": true - }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true - }, - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "lsmod": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz", - "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks=" - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, - "map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - } - }, - "merge": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", - "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "mime-db": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", - "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" - }, - "mime-types": { - "version": "2.1.19", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", - "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", - "requires": { - "mime-db": "~1.35.0" - } - }, - "mimos": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimos/-/mimos-4.0.0.tgz", - "integrity": "sha512-JvlvRLqGIlk+AYypWrbrDmhsM+6JVx/xBM5S3AMwTBz1trPCEoPN/swO2L4Wu653fL7oJdgk8DMQyG/Gq3JkZg==", - "requires": { - "hoek": "5.x.x", - "mime-db": "1.x.x" - }, - "dependencies": { - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - } - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "minimist-options": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", - "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0" - } - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, - "mocha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", - "dev": true, - "requires": { - "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.5", - "he": "1.1.1", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "supports-color": "5.4.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "modify-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", - "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", - "dev": true - }, - "module-not-found-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", - "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", - "dev": true - }, - "moment": { - "version": "2.19.3", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.3.tgz", - "integrity": "sha1-vbmdJw1tf9p4zA+6zoVeJ/59pp8=" - }, - "mozlog": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/mozlog/-/mozlog-2.2.0.tgz", - "integrity": "sha1-Rwk8XHEuKBDecnCYMFFk0SyK6SM=", - "requires": { - "intel": "^1.0.0", - "merge": "^1.2.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "multimatch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", - "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", - "dev": true, - "requires": { - "array-differ": "^1.0.0", - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "minimatch": "^3.0.0" - } - }, - "mute-stream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", - "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", - "dev": true - }, - "mysql": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.15.0.tgz", - "integrity": "sha512-C7tjzWtbN5nzkLIV+E8Crnl9bFyc7d3XJcBAvHKEVkjrYjogz3llo22q6s/hw+UcsE4/844pDob9ac+3dVjQSA==", - "requires": { - "bignumber.js": "4.0.4", - "readable-stream": "2.3.3", - "safe-buffer": "5.1.1", - "sqlstring": "2.3.0" - } - }, - "mysql-patcher": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/mysql-patcher/-/mysql-patcher-0.7.0.tgz", - "integrity": "sha1-DXUglBAFxMF3gOmIfFjEBt2GYQs=", - "requires": { - "async": "^0.9.0", - "bluebird": "^2.3.0", - "clone": "^0.1.18", - "glob": "^5.0.3", - "xtend": "^4.0.0" - }, - "dependencies": { - "bluebird": { - "version": "2.11.0", - "resolved": "http://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", - "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" - } - } - }, - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "newrelic": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/newrelic/-/newrelic-2.2.2.tgz", - "integrity": "sha1-6olr0tOAOmp3+b48/4goRcy/T7A=", - "requires": { - "@newrelic/native-metrics": "^2.1.0", - "concat-stream": "^1.5.0", - "https-proxy-agent": "^0.3.5", - "json-stringify-safe": "^5.0.0", - "readable-stream": "^2.1.4", - "semver": "^5.3.0" - } - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, - "nigel": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/nigel/-/nigel-3.0.1.tgz", - "integrity": "sha512-kCVtUG9JyD//tsYrZY+/Y+2gUrANVSba8y23QkM5Znx0FOxlnl9Z4OVPBODmstKWTOvigfTO+Va1VPOu3eWSOQ==", - "requires": { - "hoek": "5.x.x", - "vise": "3.x.x" - }, - "dependencies": { - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - } - } - }, - "nise": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.6.tgz", - "integrity": "sha512-1GedetLKzmqmgwabuMSqPsT7oumdR77SBpDfNNJhADRIeA3LN/2RVqR4fFqwvzhAqcTef6PPCzQwITE/YQ8S8A==", - "dev": true, - "requires": { - "@sinonjs/formatio": "3.0.0", - "just-extend": "^3.0.0", - "lolex": "^2.3.2", - "path-to-regexp": "^1.7.0", - "text-encoding": "^0.6.4" - }, - "dependencies": { - "lolex": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", - "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", - "dev": true - } - } - }, - "nock": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/nock/-/nock-8.2.2.tgz", - "integrity": "sha512-f4s5qR4Eg/NgaLuBYTThc/abl5mohCgIvnGdHkoqR5WgRe5amjFQTU2aia085OE8o3OAY7ZerDkRAeXfR720TA==", - "dev": true, - "requires": { - "chai": ">=1.9.2 <4.0.0", - "debug": "^2.2.0", - "deep-equal": "^1.0.0", - "json-stringify-safe": "^5.0.1", - "lodash": "~4.9.0", - "mkdirp": "^0.5.0", - "propagate": "0.4.0", - "qs": "^6.0.2" - }, - "dependencies": { - "lodash": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.9.0.tgz", - "integrity": "sha1-TCDXQvA86F3HAODderm8q4Xm/BQ=", - "dev": true - } - } - }, - "nodemon": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.18.3.tgz", - "integrity": "sha512-XdVfAjGlDKU2nqoGgycxTndkJ5fdwvWJ/tlMGk2vHxMZBrSPVh86OM6z7viAv8BBJWjMgeuYQBofzr6LUoi+7g==", - "dev": true, - "requires": { - "chokidar": "^2.0.2", - "debug": "^3.1.0", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.0", - "semver": "^5.5.0", - "supports-color": "^5.2.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^2.3.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "npmshrink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npmshrink/-/npmshrink-1.0.1.tgz", - "integrity": "sha1-vJjiXxs4/2T8qNU+hDHLTtDI+W8=", - "dev": true - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "nyc": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-13.1.0.tgz", - "integrity": "sha512-3GyY6TpQ58z9Frpv4GMExE1SV2tAgYqC7HSy2omEhNiCT3mhT9NyiOvIE8zkbuJVFzmvvNTnE4h/7/wQae7xLg==", - "dev": true, - "requires": { - "archy": "^1.0.0", - "arrify": "^1.0.1", - "caching-transform": "^2.0.0", - "convert-source-map": "^1.6.0", - "debug-log": "^1.0.1", - "find-cache-dir": "^2.0.0", - "find-up": "^3.0.0", - "foreground-child": "^1.5.6", - "glob": "^7.1.3", - "istanbul-lib-coverage": "^2.0.1", - "istanbul-lib-hook": "^2.0.1", - "istanbul-lib-instrument": "^3.0.0", - "istanbul-lib-report": "^2.0.2", - "istanbul-lib-source-maps": "^2.0.1", - "istanbul-reports": "^2.0.1", - "make-dir": "^1.3.0", - "merge-source-map": "^1.1.0", - "resolve-from": "^4.0.0", - "rimraf": "^2.6.2", - "signal-exit": "^3.0.2", - "spawn-wrap": "^1.4.2", - "test-exclude": "^5.0.0", - "uuid": "^3.3.2", - "yargs": "11.1.0", - "yargs-parser": "^9.0.2" - }, - "dependencies": { - "align-text": { - "version": "0.1.4", - "bundled": true, - "dev": true, - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - } - }, - "amdefine": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "ansi-regex": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "append-transform": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "default-require-extensions": "^2.0.0" - } - }, - "archy": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "arrify": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "async": { - "version": "1.5.2", - "bundled": true, - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "builtin-modules": { - "version": "1.1.1", - "bundled": true, - "dev": true - }, - "caching-transform": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "make-dir": "^1.0.0", - "md5-hex": "^2.0.0", - "package-hash": "^2.0.0", - "write-file-atomic": "^2.0.0" - } - }, - "camelcase": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true - }, - "center-align": { - "version": "0.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - } - }, - "cliui": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "commondir": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "convert-source-map": { - "version": "1.6.0", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "cross-spawn": { - "version": "4.0.2", - "bundled": true, - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, - "debug": { - "version": "3.1.0", - "bundled": true, - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "debug-log": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "decamelize": { - "version": "1.2.0", - "bundled": true, - "dev": true - }, - "default-require-extensions": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "strip-bom": "^3.0.0" - } - }, - "error-ex": { - "version": "1.3.2", - "bundled": true, - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es6-error": { - "version": "4.1.1", - "bundled": true, - "dev": true - }, - "execa": { - "version": "0.7.0", - "bundled": true, - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "5.1.0", - "bundled": true, - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - } - } - }, - "find-cache-dir": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^1.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "foreground-child": { - "version": "1.5.6", - "bundled": true, - "dev": true, - "requires": { - "cross-spawn": "^4", - "signal-exit": "^3.0.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "get-caller-file": { - "version": "1.0.3", - "bundled": true, - "dev": true - }, - "get-stream": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true, - "dev": true - }, - "handlebars": { - "version": "4.0.11", - "bundled": true, - "dev": true, - "requires": { - "async": "^1.4.0", - "optimist": "^0.6.1", - "source-map": "^0.4.4", - "uglify-js": "^2.6" - }, - "dependencies": { - "source-map": { - "version": "0.4.4", - "bundled": true, - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "has-flag": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "hosted-git-info": { - "version": "2.7.1", - "bundled": true, - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "bundled": true, - "dev": true - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "invert-kv": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "is-arrayish": { - "version": "0.2.1", - "bundled": true, - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "bundled": true, - "dev": true - }, - "is-builtin-module": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "builtin-modules": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "isexe": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "istanbul-lib-coverage": { - "version": "2.0.1", - "bundled": true, - "dev": true - }, - "istanbul-lib-hook": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "requires": { - "append-transform": "^1.0.0" - } - }, - "istanbul-lib-report": { - "version": "2.0.2", - "bundled": true, - "dev": true, - "requires": { - "istanbul-lib-coverage": "^2.0.1", - "make-dir": "^1.3.0", - "supports-color": "^5.4.0" - } - }, - "istanbul-lib-source-maps": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "requires": { - "debug": "^3.1.0", - "istanbul-lib-coverage": "^2.0.1", - "make-dir": "^1.3.0", - "rimraf": "^2.6.2", - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "bundled": true, - "dev": true - } - } - }, - "istanbul-reports": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "requires": { - "handlebars": "^4.0.11" - } - }, - "json-parse-better-errors": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "bundled": true, - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - }, - "lazy-cache": { - "version": "1.0.4", - "bundled": true, - "dev": true, - "optional": true - }, - "lcid": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "invert-kv": "^1.0.0" - } - }, - "load-json-file": { - "version": "4.0.0", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash.flattendeep": { - "version": "4.4.0", - "bundled": true, - "dev": true - }, - "longest": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "lru-cache": { - "version": "4.1.3", - "bundled": true, - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "make-dir": { - "version": "1.3.0", - "bundled": true, - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "md5-hex": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "md5-o-matic": "^0.1.1" - } - }, - "md5-o-matic": { - "version": "0.1.1", - "bundled": true, - "dev": true - }, - "mem": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "merge-source-map": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "requires": { - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "bundled": true, - "dev": true - } - } - }, - "mimic-fn": { - "version": "1.2.0", - "bundled": true, - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.10", - "bundled": true, - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - } - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "normalize-package-data": { - "version": "2.4.0", - "bundled": true, - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "npm-run-path": { - "version": "2.0.2", - "bundled": true, - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optimist": { - "version": "0.6.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "os-locale": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "requires": { - "execa": "^0.7.0", - "lcid": "^1.0.0", - "mem": "^1.1.0" - } - }, - "p-finally": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "p-limit": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "package-hash": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "lodash.flattendeep": "^4.4.0", - "md5-hex": "^2.0.0", - "release-zalgo": "^1.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "bundled": true, - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-exists": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "path-key": { - "version": "2.0.1", - "bundled": true, - "dev": true - }, - "path-type": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "pkg-dir": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, - "pseudomap": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "4.0.0", - "bundled": true, - "dev": true, - "requires": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" - } - }, - "release-zalgo": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "es6-error": "^4.0.1" - } - }, - "repeat-string": { - "version": "1.6.1", - "bundled": true, - "dev": true - }, - "require-directory": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "bundled": true, - "dev": true - }, - "right-align": { - "version": "0.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.1" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true - }, - "source-map": { - "version": "0.5.7", - "bundled": true, - "dev": true, - "optional": true - }, - "spawn-wrap": { - "version": "1.4.2", - "bundled": true, - "dev": true, - "requires": { - "foreground-child": "^1.5.6", - "mkdirp": "^0.5.0", - "os-homedir": "^1.0.1", - "rimraf": "^2.6.2", - "signal-exit": "^3.0.2", - "which": "^1.3.0" - } - }, - "spdx-correct": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.1.0", - "bundled": true, - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "string-width": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "strip-eof": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "supports-color": { - "version": "5.4.0", - "bundled": true, - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "test-exclude": { - "version": "5.0.0", - "bundled": true, - "dev": true, - "requires": { - "arrify": "^1.0.1", - "minimatch": "^3.0.4", - "read-pkg-up": "^4.0.0", - "require-main-filename": "^1.0.1" - } - }, - "uglify-js": { - "version": "2.8.29", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - }, - "dependencies": { - "yargs": { - "version": "3.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - } - } - } - }, - "uglify-to-browserify": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "uuid": { - "version": "3.3.2", - "bundled": true, - "dev": true - }, - "validate-npm-package-license": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "which": { - "version": "1.3.1", - "bundled": true, - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "window-size": { - "version": "0.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "wordwrap": { - "version": "0.0.3", - "bundled": true, - "dev": true - }, - "wrap-ansi": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "write-file-atomic": { - "version": "2.3.0", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "y18n": { - "version": "3.2.1", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "2.1.2", - "bundled": true, - "dev": true - }, - "yargs": { - "version": "11.1.0", - "bundled": true, - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.1.1", - "find-up": "^2.1.0", - "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^9.0.2" - }, - "dependencies": { - "cliui": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "bundled": true, - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "bundled": true, - "dev": true - } - } - }, - "yargs-parser": { - "version": "9.0.2", - "bundled": true, - "dev": true, - "requires": { - "camelcase": "^4.1.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "bundled": true, - "dev": true - } - } - } - } - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - } - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - }, - "dependencies": { - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - } - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", - "dev": true, - "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - } - }, - "parse-github-repo-url": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", - "integrity": "sha1-nn2LslKmy2ukJZUGC3v23z28H1A=", - "dev": true - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "parse-ms": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-1.0.1.tgz", - "integrity": "sha1-VjRtR0nXjyNDDKDHE4UK75GqNh0=", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "dev": true, - "requires": { - "isarray": "0.0.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - } - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", - "dev": true, - "requires": { - "through": "~2.3" - } - }, - "pem-jwk": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pem-jwk/-/pem-jwk-1.5.1.tgz", - "integrity": "sha1-eoY3/S9nqCflfAxC4cI8P9Us+wE=", - "requires": { - "asn1.js": "1.0.3" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "pez": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/pez/-/pez-4.0.2.tgz", - "integrity": "sha512-HuPxmGxHsEFPWhdkwBs2gIrHhFqktIxMtudISTFN95RQ85ZZAOl8Ki6u3nnN/X8OUaGlIGldk/l8p2IR4/i76w==", - "requires": { - "b64": "4.x.x", - "boom": "7.x.x", - "content": "4.x.x", - "hoek": "5.x.x", - "nigel": "3.x.x" - }, - "dependencies": { - "boom": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-7.2.0.tgz", - "integrity": "sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY=", - "requires": { - "hoek": "5.x.x" - } - }, - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - } - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "pkg-up": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-1.0.0.tgz", - "integrity": "sha1-Pgj7RhUlxEIWJKM7n35tCvWwWiY=", - "dev": true, - "requires": { - "find-up": "^1.0.0" - } - }, - "plur": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz", - "integrity": "sha1-dIJFLBoPUI4+NE6uwxLJHCncZVo=", - "dev": true, - "requires": { - "irregular-plurals": "^1.0.0" - } - }, - "pluralize": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", - "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", - "dev": true - }, - "podium": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/podium/-/podium-3.1.2.tgz", - "integrity": "sha512-18VrjJAduIdPv7d9zWsfmKxTj3cQTYC5Pv5gtKxcWujYBpGbV+mhNSPYhlHW5xeWoazYyKfB9FEsPT12r5rY1A==", - "requires": { - "hoek": "5.x.x", - "joi": "13.x.x" - }, - "dependencies": { - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - }, - "joi": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz", - "integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==", - "requires": { - "hoek": "5.x.x", - "isemail": "3.x.x", - "topo": "3.x.x" - } - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true - }, - "pretty-ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-2.1.0.tgz", - "integrity": "sha1-QlfCVt8/sLRR1q/6qwIYhBJpgdw=", - "dev": true, - "requires": { - "is-finite": "^1.0.1", - "parse-ms": "^1.0.0", - "plur": "^1.0.0" - }, - "dependencies": { - "plur": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/plur/-/plur-1.0.0.tgz", - "integrity": "sha1-24XGgU9eXlo7Se/CjWBP7GKXUVY=", - "dev": true - } - } - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", - "dev": true - }, - "propagate": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-0.4.0.tgz", - "integrity": "sha1-8/zKCm/gZzanulcpZgaWF8EwtIE=", - "dev": true - }, - "proxyquire": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-1.8.0.tgz", - "integrity": "sha1-AtUUpb7ZhvBMuyCTrxZ0FTX3ntw=", - "dev": true, - "requires": { - "fill-keys": "^1.0.2", - "module-not-found-error": "^1.0.0", - "resolve": "~1.1.7" - } - }, - "ps-tree": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", - "integrity": "sha1-tCGyQUDWID8e08dplrRCewjowBQ=", - "dev": true, - "requires": { - "event-stream": "~3.3.0" - } - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "pstree.remy": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.0.tgz", - "integrity": "sha512-q5I5vLRMVtdWa8n/3UEzZX7Lfghzrg9eG2IKk2ENLSofKRCXVqMvMUHxCKgXNaqH/8ebhBxrqftHWnyTFweJ5Q==", - "dev": true, - "requires": { - "ps-tree": "^1.1.0" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "quick-lru": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", - "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", - "dev": true - }, - "raven": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/raven/-/raven-2.2.1.tgz", - "integrity": "sha1-V8f75oqAFH7FJ97z18AVdc+Uj+M=", - "requires": { - "cookie": "0.3.1", - "lsmod": "1.0.0", - "stack-trace": "0.0.9", - "timed-out": "4.0.1", - "uuid": "3.0.0" - }, - "dependencies": { - "stack-trace": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", - "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=" - } - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - } - } - }, - "read": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", - "dev": true, - "requires": { - "mute-stream": "~0.0.4" - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.0.3", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "minimatch": "^3.0.2", - "readable-stream": "^2.0.2", - "set-immediate-shim": "^1.0.1" - } - }, - "readline2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", - "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "mute-stream": "0.0.5" - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, - "redeyed": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-1.0.1.tgz", - "integrity": "sha1-6WwZO0DAgWsArshCaY5hGF5VSYo=", - "optional": true, - "requires": { - "esprima": "~3.0.0" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "registry-auth-token": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", - "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", - "dev": true, - "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "dev": true, - "requires": { - "rc": "^1.0.1" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "^1.0.0" - } - }, - "request": { - "version": "2.85.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", - "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.6.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.1", - "forever-agent": "~0.6.1", - "form-data": "~2.3.1", - "har-validator": "~5.0.3", - "hawk": "~6.0.2", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.17", - "oauth-sign": "~0.8.2", - "performance-now": "^2.1.0", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1", - "stringstream": "~0.0.5", - "tough-cookie": "~2.3.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.1.0" - }, - "dependencies": { - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - } - } - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" - } - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - }, - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - }, - "resolve-pkg": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/resolve-pkg/-/resolve-pkg-0.1.0.tgz", - "integrity": "sha1-AsyZNBDik2livZcWahsHfalyVTE=", - "dev": true, - "requires": { - "resolve-from": "^2.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", - "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=", - "dev": true - } - } - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "dev": true, - "requires": { - "exit-hook": "^1.0.0", - "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", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.1" - } - }, - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", - "dev": true - }, - "run-async": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", - "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", - "dev": true, - "requires": { - "once": "^1.3.0" - } - }, - "rx-lite": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", - "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", - "dev": true - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "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", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sax": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/sax/-/sax-0.4.2.tgz", - "integrity": "sha1-OfO2AXM9a+yXEFskKipA/Wl4rDw=" - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" - }, - "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "dev": true, - "requires": { - "semver": "^5.0.3" - } - }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shelljs": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz", - "integrity": "sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg=", - "dev": true - }, - "shot": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/shot/-/shot-4.0.5.tgz", - "integrity": "sha1-x+dFXRHWD2ts08Q+FaO0McF+VWY=", - "requires": { - "hoek": "5.x.x", - "joi": "13.x.x" - }, - "dependencies": { - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - }, - "joi": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz", - "integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==", - "requires": { - "hoek": "5.x.x", - "isemail": "3.x.x", - "topo": "3.x.x" - } - } - } - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "sinon": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.0.0.tgz", - "integrity": "sha512-8OrSYFPZ9xaECfi1ayVMd0ihYCW2OZYgX3rBczrB990gHZMM+aftvhNTJazGz/luS0Us9NWgD5P3KGQ7kYZvGg==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.0.2", - "@sinonjs/formatio": "^3.0.0", - "@sinonjs/samsam": "^2.1.2", - "diff": "^3.5.0", - "lodash.get": "^4.4.2", - "lolex": "^3.0.0", - "nise": "^1.4.5", - "supports-color": "^5.5.0", - "type-detect": "^4.0.8" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - } - } - }, - "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - } - }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "requires": { - "hoek": "4.x.x" - } - }, - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "spdx-correct": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", - "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", - "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", - "dev": true - }, - "split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "dev": true, - "requires": { - "through": "2" - } - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "split2": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", - "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", - "dev": true, - "requires": { - "through2": "^2.0.2" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "sqlstring": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.0.tgz", - "integrity": "sha1-UluKT9Jtb3GqYegipsr5dtMa0qg=" - }, - "sshpk": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", - "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" - }, - "statehood": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/statehood/-/statehood-6.0.6.tgz", - "integrity": "sha512-jR45n5ZMAkasw0xoE9j9TuLmJv4Sa3AkXe+6yIFT6a07kXYHgSbuD2OVGECdZGFxTmvNqLwL1iRIgvq6O6rq+A==", - "requires": { - "boom": "7.x.x", - "bounce": "1.x.x", - "cryptiles": "4.x.x", - "hoek": "5.x.x", - "iron": "5.x.x", - "joi": "13.x.x" - }, - "dependencies": { - "boom": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-7.2.0.tgz", - "integrity": "sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY=", - "requires": { - "hoek": "5.x.x" - } - }, - "cryptiles": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-4.1.2.tgz", - "integrity": "sha512-U2ALcoAHvA1oO2xOreyHvtkQ+IELqDG2WVWRI1GH/XEmmfGIOalnM5MU5Dd2ITyWfr3m6kNqXiy8XuYyd4wKJw==", - "requires": { - "boom": "7.x.x" - } - }, - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - }, - "joi": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz", - "integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==", - "requires": { - "hoek": "5.x.x", - "isemail": "3.x.x", - "topo": "3.x.x" - } - } - } - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", - "dev": true, - "requires": { - "duplexer": "~0.1.1" - } - }, - "strftime": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/strftime/-/strftime-0.10.0.tgz", - "integrity": "sha1-s/D6QZKVICpaKJ9ta+n0kJphcZM=" - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "stringstream": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", - "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1" - } - }, - "strip-json-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", - "dev": true - }, - "subtext": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/subtext/-/subtext-6.0.7.tgz", - "integrity": "sha512-IcJUvRjeR+NB437Iq+LORFNJW4L6Knqkj3oQrBrkdhIaS2VKJvx/9aYEq7vi+PEx5/OuehOL/40SkSZotLi/MA==", - "requires": { - "boom": "7.x.x", - "content": "4.x.x", - "hoek": "5.x.x", - "pez": "4.x.x", - "wreck": "14.x.x" - }, - "dependencies": { - "boom": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-7.2.0.tgz", - "integrity": "sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY=", - "requires": { - "hoek": "5.x.x" - } - }, - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - } - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "symbol": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/symbol/-/symbol-0.3.1.tgz", - "integrity": "sha1-tvmpANSWpX8CQI8iGYwQndoGMEE=" - }, - "table": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", - "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", - "dev": true, - "requires": { - "ajv": "^4.7.0", - "ajv-keywords": "^1.0.0", - "chalk": "^1.1.1", - "lodash": "^4.0.0", - "slice-ansi": "0.0.4", - "string-width": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "dev": true, - "requires": { - "co": "^4.6.0", - "json-stable-stringify": "^1.0.1" - } - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "teamwork": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/teamwork/-/teamwork-3.0.1.tgz", - "integrity": "sha512-hEkJIpDOfOYe9NYaLFk00zQbzZeKNCY8T2pRH3I13Y1mJwxaSQ6NEsjY5rCp+11ezCiZpWGoGFTbOuhg4qKevQ==" - }, - "term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "dev": true, - "requires": { - "execa": "^0.7.0" - } - }, - "text-encoding": { - "version": "0.6.4", - "resolved": "http://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", - "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", - "dev": true - }, - "text-extensions": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.7.0.tgz", - "integrity": "sha512-AKXZeDq230UaSzaO5s3qQUZOaC7iKbzq0jOFL614R7d9R593HLqAOL0cYoqLdkNrjBSOdmoQI06yigq1TSBXAg==", - "dev": true - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", - "dev": true, - "requires": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" - } - }, - "time-grunt": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/time-grunt/-/time-grunt-1.4.0.tgz", - "integrity": "sha1-BiIT5mDJB+hvRAVWwB6mWXtxJCA=", - "dev": true, - "requires": { - "chalk": "^1.0.0", - "date-time": "^1.1.0", - "figures": "^1.0.0", - "hooker": "^0.2.3", - "number-is-nan": "^1.0.0", - "pretty-ms": "^2.1.0", - "text-table": "^0.2.0" - } - }, - "time-zone": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-0.1.0.tgz", - "integrity": "sha1-Sncotqwo2w4Aj1FAQ/1VW9VXO0Y=", - "dev": true - }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "topo": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.0.tgz", - "integrity": "sha512-Tlu1fGlR90iCdIPURqPiufqAlCZYzLjHYVVbcFWDMcX7+tK8hdZWAfsMrD/pBul9jqHHwFjNdf1WaxA9vTRRhw==", - "requires": { - "hoek": "5.x.x" - }, - "dependencies": { - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - } - } - }, - "touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "requires": { - "nopt": "~1.0.10" - }, - "dependencies": { - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "dev": true, - "requires": { - "abbrev": "1" - } - } - } - }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", - "requires": { - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - } - } - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true - }, - "trim-off-newlines": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", - "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", - "dev": true - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-detect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", - "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", - "dev": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "dev": true, - "optional": true, - "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "optional": true - } - } - }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "dev": true, - "optional": true - }, - "undefsafe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", - "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", - "dev": true, - "requires": { - "debug": "^2.2.0" - } - }, - "underscore.string": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", - "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==", - "dev": true, - "requires": { - "sprintf-js": "^1.0.3", - "util-deprecate": "^1.0.2" - } - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } - } - }, - "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "dev": true, - "requires": { - "crypto-random-string": "^1.0.0" - } - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", - "dev": true - }, - "upath": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", - "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", - "dev": true - }, - "update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", - "dev": true, - "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "urijs": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", - "integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==" - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "dev": true, - "requires": { - "prepend-http": "^1.0.1" - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "user-home": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0" - } - }, - "utcstring": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/utcstring/-/utcstring-0.1.0.tgz", - "integrity": "sha1-Qw/VEKt/yVtdWRDJAteYgMIIQ2s=" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "uuid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz", - "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg=" - }, - "validate-npm-package-license": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", - "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "validator": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-7.2.0.tgz", - "integrity": "sha512-c8NGTUYeBEcUIGeMppmNVKHE7wwfm3mYbNZxV+c5mlv9fDHI7Ad3p07qfNrn/CvpdkK2k61fOLRO2sTEhgQXmg==" - }, - "varify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/varify/-/varify-0.2.0.tgz", - "integrity": "sha1-GR2p/p3EzWjQ0USY1OKpEP9OZRY=", - "optional": true, - "requires": { - "redeyed": "~1.0.1", - "through": "~2.3.4" - } - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vise": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vise/-/vise-3.0.0.tgz", - "integrity": "sha512-kBFZLmiL1Vm3rHXphkhvvAcsjgeQXRrOFCbJb0I50YZZP4HGRNH+xGzK3matIMcpbsfr3I02u9odj4oCD0TWgA==", - "requires": { - "hoek": "5.x.x" - }, - "dependencies": { - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - } - } - }, - "which": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", - "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "widest-line": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.0.tgz", - "integrity": "sha1-AUKk6KJD+IgsAjOqDgKBqnYVInM=", - "dev": true, - "requires": { - "string-width": "^2.1.1" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "dev": true, - "optional": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "wreck": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/wreck/-/wreck-14.1.0.tgz", - "integrity": "sha512-y/iwFhwdGoM8Hk1t1I4LbuLhM3curVD8STd5NcFI0c/4b4cQAMLcnCRxXX9sLQAggDC8dXYSaQNsT64hga6lvA==", - "requires": { - "boom": "7.x.x", - "hoek": "5.x.x" - }, - "dependencies": { - "boom": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-7.2.0.tgz", - "integrity": "sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY=", - "requires": { - "hoek": "5.x.x" - } - }, - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - } - } - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, - "write-file-atomic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", - "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", - "dev": true - }, - "xml2js": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.6.tgz", - "integrity": "sha1-0gnE5N2h/JxFIUHvQcB39a399sQ=", - "requires": { - "sax": "0.4.2" - } - }, - "xmlbuilder": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz", - "integrity": "sha1-F3bWXz/brUcKCNhgTN6xxOVA/4M=" - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, - "optional": true, - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - }, - "dependencies": { - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true, - "optional": true - } - } - }, - "yargs-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", - "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", - "requires": { - "camelcase": "^4.1.0" - } - } - } -} diff --git a/fxa-oauth-server/package.json b/fxa-oauth-server/package.json deleted file mode 100644 index a77e429..0000000 --- a/fxa-oauth-server/package.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "name": "fxa-oauth-server", - "version": "1.123.0", - "private": true, - "description": "Firefox Accounts OAuth2 server.", - "scripts": { - "start": "grunt server --node-env=dev", - "test": "scripts/test-local.sh", - "outdated": "npm outdated --depth 0 || exit 0", - "postinstall": "node scripts/gen_keys", - "shrink": "npmshrink" - }, - "repository": { - "type": "git", - "url": "git://github.com/mozilla/fxa-oauth-server" - }, - "author": "Mozilla (https://mozilla.org)", - "homepage": "https://github.com/mozilla/fxa-oauth-server", - "license": "MPL-2.0", - "bugs": { - "url": "https://github.com/mozilla/fxa-oauth-server/issues" - }, - "engines": { - "node": ">=8" - }, - "dependencies": { - "bluebird": "3.5.2", - "buf": "0.1.0", - "commander": "2.15.1", - "convict": "4.0.2", - "fxa-jwtool": "0.7.2", - "fxa-notifier-aws": "1.0.0", - "fxa-shared": "1.0.13", - "hapi": "17.6.0", - "hapi-hpkp": "2.0.0", - "joi": "14.0.0", - "keypair": "1.0.1", - "mozlog": "2.2.0", - "mysql": "2.15.0", - "mysql-patcher": "0.7.0", - "newrelic": "2.2.2", - "raven": "2.2.1", - "request": "2.85.0", - "urijs": "1.19.1" - }, - "devDependencies": { - "eslint-plugin-fxa": "git://github.com/mozilla/eslint-plugin-fxa.git#41504c9dd30e8b52900c15b524946aa0428aef95", - "fxa-conventional-changelog": "1.1.0", - "grunt": "1.0.3", - "grunt-bump": "0.8.0", - "grunt-conventional-changelog": "6.1.0", - "grunt-copyright": "0.3.0", - "grunt-eslint": "18.0.0", - "grunt-nodemon": "0.4.2", - "insist": "1.x", - "load-grunt-tasks": "3.5.2", - "mocha": "5.2.0", - "nock": "8.2.2", - "npmshrink": "1.0.1", - "nyc": "13.1.0", - "proxyquire": "1.8.0", - "read": "1.0.7", - "sinon": "7.0.0", - "time-grunt": "1.4.0" - } -} diff --git a/fxa-oauth-server/scripts/.eslintrc b/fxa-oauth-server/scripts/.eslintrc deleted file mode 100644 index c843a34..0000000 --- a/fxa-oauth-server/scripts/.eslintrc +++ /dev/null @@ -1,8 +0,0 @@ -plugins: - - fxa - -rules: - fxa/async-crypto-random: 0 - fxa/no-new-buffer: 2 - no-console: 0 - handle-callback-err: 0 diff --git a/fxa-oauth-server/scripts/create-token-test-rows.js b/fxa-oauth-server/scripts/create-token-test-rows.js deleted file mode 100644 index 77ae03e..0000000 --- a/fxa-oauth-server/scripts/create-token-test-rows.js +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env node -/* 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/. */ - -// Create token table rows with varying expiresAt values for use in testing -// ../bin/purge_expired_tokens.js - -const crypto = require('crypto'); -const util = require('util'); - -const TEST_CLIENT_IDS = [ - 'deadbeefdeadbe00', - 'deadbeefdeadbe11', - 'deadbeefdeadbe22', - 'deadbeefdeadbe33', -]; - -function randomHash(length) { - return crypto.randomBytes(length).toString('hex'); -} - -function expiresAt(yearsWindow = 3) { - const now = Date.now(); - const oneYearMs = 365 * 86400 * 1000; - const windowSize = yearsWindow * oneYearMs; - const epochBase = now - windowSize; - const expiresEpoch = Math.floor(epochBase + Math.random() * windowSize); - - return new Date(expiresEpoch).toISOString() - .replace('T', ' ').replace(/\.\d{3}Z/, ''); -} - -function clientId() { - const index = Math.floor(Math.random() * TEST_CLIENT_IDS.length); - return TEST_CLIENT_IDS[index]; -} - -function makeInsertStatement() { - const data = { - token: randomHash(32), - clientId: clientId(), - userId: randomHash(16), - email: 'test@example.com', - type: 'bearer', - scope: 'oauth', - expiresAt: expiresAt() - }; - - const values = util.format("X'%s',X'%s',X'%s','%s','%s','%s','%s'", - data.token, - data.clientId, - data.userId, - data.email, - data.type, - data.scope, - data.expiresAt); - - const insertFmt = 'INSERT INTO tokens (token, clientId, userId, email, type, scope, expiresAt) VALUES (%s);\n'; - const insert = util.format(insertFmt, values); - - return insert; -} - -function main() { - console.log('DELETE FROM tokens; DELETE FROM clients;\n'); - - const clientIdFmt = "REPLACE INTO clients VALUES(X'%s', '%s', '', '', 0, 0, NOW(), 0, '', X'%s', X'%s');\n"; - TEST_CLIENT_IDS.forEach((id) => { - process.stdout.write(util.format(clientIdFmt, id, id, randomHash(32), randomHash(32))); - }); - - const rowCount = Number(process.env.ROW_COUNT) || 1000; - for (let i = 0; i < rowCount; ++i) { - process.stdout.write(makeInsertStatement()); - } -} - -main(); - - diff --git a/fxa-oauth-server/scripts/gen_keys.js b/fxa-oauth-server/scripts/gen_keys.js deleted file mode 100644 index cc8338d..0000000 --- a/fxa-oauth-server/scripts/gen_keys.js +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env node - -/* 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/. */ - -/* - - Usage: - scripts/gen_keys.js - - Will create these files - - ./config/key.json - ./config/oldKey.json - - If these files already exist, this script will show an error message - and exit. You must remove both keys if you want to generate a new - keypair. - */ - -const fs = require('fs'); -const assert = require('assert'); -const crypto = require('crypto'); -const generateRSAKeypair = require('keypair'); -const JwTool = require('fxa-jwtool'); - -const keyPath = './config/key.json'; -const oldKeyPath = './config/oldKey.json'; - -try { - var keysExist = fs.existsSync(keyPath) && fs.existsSync(oldKeyPath); - assert(! keysExist, 'keys already exists'); -} catch (e) { - process.exit(); -} - -// We tag our keys with their creation time, and a unique key id -// based on a hash of the public key and the timestamp. The result -// comes out like: -// { -// kid: "2017-03-16-ebe69008de771d62cd1cadf9faa6daae" -// "fxa-createdAt": 1489716000, -// } -function makeKeyProperties(kp) { - var now = new Date(); - return { - // Key id based on timestamp and hash of public key. - kid: now.toISOString().slice(0, 10) + '-' + - crypto.createHash('sha256').update(kp.public).digest('hex').slice(0, 32), - // Timestamp to nearest hour; consumers don't need to know the precise time. - 'fxa-createdAt': Math.round(now / 1000 / 3600) * 3600 - }; -} - -function main(cb) { - var kp = generateRSAKeypair(); - var privKey = JwTool.JWK.fromPEM(kp.private, makeKeyProperties(kp)); - try { - fs.mkdirSync('./config'); - } catch (accessEx) { - - } - - fs.writeFileSync(keyPath, JSON.stringify(privKey.toJSON(), undefined, 2)); - console.log('Key saved:', keyPath); //eslint-disable-line no-console - - // The "old key" is not used to sign anything, so we don't need to store - // the private component, we just need to serve the public component - // so that old signatures can be verified correctly. - kp = generateRSAKeypair(); - var pubKey = JwTool.JWK.fromPEM(kp.public, makeKeyProperties(kp)); - fs.writeFileSync(oldKeyPath, JSON.stringify(pubKey.toJSON(), undefined, 2)); - console.log('OldKey saved:', oldKeyPath); //eslint-disable-line no-console - cb(); -} - -module.exports = main; - -if (require.main === module) { - main(function () { - }); -} diff --git a/fxa-oauth-server/scripts/generate-client-for-ops.js b/fxa-oauth-server/scripts/generate-client-for-ops.js deleted file mode 100644 index 83ac6dc..0000000 --- a/fxa-oauth-server/scripts/generate-client-for-ops.js +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env node - -/* 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/. */ - -if (process.argv.length !== 4) { - console.error('Usage: node generate-client-for-ops.js "client-name" "client-redirect-url"'); - process.exit(1); -} - -var crypto = require('crypto'); - -var id = crypto.randomBytes(8).toString('hex'); -var secret = crypto.randomBytes(32); -var hashedSecret = crypto.createHash('sha256').update(secret).digest('hex'); -secret = secret.toString('hex'); - -var client = { - id: id, - name: process.argv[2], - hashedSecret: hashedSecret, - redirectUri: process.argv[3], - imageUri: '', - canGrant: false, - termsUri: '', - privacyUri: '', - trusted: true -}; - -console.log('# Secret to GPG encrypt and send to requestor #'); -console.log(secret); -console.log(); - -console.log('# Data to add to deployment config #'); -console.log(JSON.stringify(client, null, 2)); -console.log(); - -var sql = `INSERT INTO clients (id, name, hashedSecret, redirectUri, imageUri, canGrant, termsUri, privacyUri, trusted) - VALUES (unhex('${client.id}'), '${client.name}', unhex('${client.hashedSecret}'), '${client.redirectUri}', '${client.imageUri}', - '${client.canGrant ? 1 : 0}', '${client.termsUri}', '${client.privacyUri}', '${client.trusted ? 1 : 0}');`; - -console.log('# Data to insert into database #'); -console.log(sql); -console.log(); diff --git a/fxa-oauth-server/scripts/mocha-coverage.js b/fxa-oauth-server/scripts/mocha-coverage.js deleted file mode 100755 index e10a057..0000000 --- a/fxa-oauth-server/scripts/mocha-coverage.js +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env node - -/* 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 path = require('path'); -const spawn = require('child_process').spawn; - -const MOCHA_BIN = path.join(path.dirname(__dirname), 'node_modules', '.bin', 'mocha'); -const NYC_BIN = path.join(path.dirname(__dirname), 'node_modules', '.bin', 'nyc'); - -let bin = NYC_BIN; -let argv = ['--cache', MOCHA_BIN]; - -if (process.env.NO_COVERAGE) { - bin = MOCHA_BIN; - argv = []; -} - -const p = spawn(bin, argv.concat(process.argv.slice(2)), { stdio: 'inherit', env: process.env }); - -// exit this process with the same exit code as the test process -p.on('close', function (code) { - process.exit(code); -}); diff --git a/fxa-oauth-server/scripts/rpm-version.js b/fxa-oauth-server/scripts/rpm-version.js deleted file mode 100755 index 4fabf81..0000000 --- a/fxa-oauth-server/scripts/rpm-version.js +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env node -/* 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/. */ - -var cp = require('child_process'); -var util = require('util'); - -// Generate legacy-format output that looks something like: -// -// { -// "version": { -// "hash": "88f6f24e53da56faa933c357bffb61cbeaec7ff3", -// "subject": "Merge pull request #2939 from vladikoff/sentry-patch3", -// "committer date": "1439472293", -// "source": "git://github.com/mozilla/fxa-content-server.git" -// } -// } -// -// This content is placed in the stage/prod rpm at `./config/version.json`. -// Ignore errors and always produce a (possibly empty struct) output. - -var args = '{"hash":"%H","subject":"%s","committer date":"%ct"}'; -var cmd = util.format('git --no-pager log --format=format:\'%s\' -1', args); -cp.exec(cmd, function (err, stdout) { // eslint-disable-line handle-callback-err - var info = { - version: JSON.parse(stdout || '{}') - }; - - var cmd = 'git config --get remote.origin.url'; - cp.exec(cmd, function (err, stdout) { // eslint-disable-line handle-callback-err - info.version.source = (stdout && stdout.trim()) || ''; - console.log(JSON.stringify(info, null, 2)); // eslint-disable-line no-console - }); -}); diff --git a/fxa-oauth-server/scripts/show-db-encoding.js b/fxa-oauth-server/scripts/show-db-encoding.js deleted file mode 100755 index b4291da..0000000 --- a/fxa-oauth-server/scripts/show-db-encoding.js +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env node - -/* 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/. */ - -const db = require('../lib/db'); - -db.getEncodingInfo() - .then(function(info) { - console.log(JSON.stringify(info, null, 2)); // eslint-disable-line no-console - }).then(process.exit); diff --git a/fxa-oauth-server/scripts/test-local.sh b/fxa-oauth-server/scripts/test-local.sh deleted file mode 100755 index 8bc6b2f..0000000 --- a/fxa-oauth-server/scripts/test-local.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -set -e - -if [ -z "$NODE_ENV" ]; then export NODE_ENV=test; fi; - -set -u - -GLOB=$* -if [ -z "$GLOB" ]; then - GLOB="test/*.js test/routes/*.js test/db/*.js" -fi - -DEFAULT_ARGS="-R spec --timeout 20000 --recursive --exit" - -./scripts/mocha-coverage.js $DEFAULT_ARGS $GLOB -grunt lint copyright diff --git a/fxa-oauth-server/test/.eslintrc b/fxa-oauth-server/test/.eslintrc deleted file mode 100644 index 983c105..0000000 --- a/fxa-oauth-server/test/.eslintrc +++ /dev/null @@ -1,10 +0,0 @@ -plugins: - - fxa -extends: ../.eslintrc - -env: - mocha: true - -rules: - fxa/async-crypto-random: 0 - fxa/no-new-buffer: 2 diff --git a/fxa-oauth-server/test/api.js b/fxa-oauth-server/test/api.js deleted file mode 100644 index f910079..0000000 --- a/fxa-oauth-server/test/api.js +++ /dev/null @@ -1,3599 +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/. */ - -const url = require('url'); -const assert = require('insist'); -const nock = require('nock'); -const buf = require('buf').hex; -const generateRSAKeypair = require('keypair'); -const JWTool = require('fxa-jwtool'); -const ScopeSet = require('fxa-shared').oauth.scopes; - -const auth = require('../lib/auth'); -const config = require('../lib/config'); -const db = require('../lib/db'); -const encrypt = require('../lib/encrypt'); -const P = require('../lib/promise'); -const Server = require('./lib/server'); -const unique = require('../lib/unique'); -const util = require('../lib/util'); - -const assertSecurityHeaders = require('./lib/util').assertSecurityHeaders; - -const USERID = unique(16).toString('hex'); -const VEMAIL = unique(4).toString('hex') + '@mozilla.com'; -const AUTH_AT = Math.floor(Date.now() / 1000); -const STALE_AUTH_AT = AUTH_AT - (2 * 24 * 60 * 60); -const AMR = ['pwd', 'email']; -const AAL = 1; -const ACR = 'AAL1'; -const PROFILE_CHANGED_AT_LATER_TIME = AUTH_AT + 1000; - -function mockVerifierResult(opts) { - opts = opts || {}; - return JSON.stringify({ - status: opts.status || 'okay', - email: (opts.uid || USERID) + '@' + config.get('browserid.issuer'), - issuer: opts.issuer || config.get('browserid.issuer'), - idpClaims: { - 'fxa-verifiedEmail': opts.vemail || VEMAIL, - 'fxa-lastAuthAt': opts.authAt || AUTH_AT, - 'fxa-generation': opts.generation || 123456, - 'fxa-tokenVerified': opts.hasOwnProperty('tokenVerified') ? opts.tokenVerified : true, - 'fxa-amr': opts.amr || AMR, - 'fxa-aal': opts.aal || AAL, - 'fxa-profileChangedAt': opts.profileChangedAt - } - }); -} - -const VERIFY_GOOD = mockVerifierResult(); -const VERIFY_GOOD_BUT_STALE = mockVerifierResult({ authAt: STALE_AUTH_AT }); -const VERIFY_GOOD_BUT_UNVERIFIED = mockVerifierResult({ tokenVerified: false }); - -const MAX_TTL_S = config.get('expiration.accessToken') / 1000; - -const JWT_PRIV_KEY = JWTool.JWK.fromObject(require('./lib/privkey.json')); -const JWT_PUB_KEY = require('./lib/pubkey.json'); -JWT_PUB_KEY.kid = 'dev-1'; -JWT_PUB_KEY.use = 'sig'; -JWT_PUB_KEY.alg = 'RS'; - -const SCOPED_CLIENT_ID = 'aaa6b9b3a65a1871'; -const NO_KEY_SCOPES_CLIENT_ID = '38a6b9b3a65a1871'; -const BAD_CLIENT_ID = '0006b9b3a65a1871'; -const SCOPE_CAN_SCOPE_KEY = 'https://identity.mozilla.com/apps/sample-scope-can-scope-key'; - - -function mockAssertion() { - var parts = url.parse(config.get('browserid.verificationUrl')); - return nock(parts.protocol + '//' + parts.host).post(parts.path); -} - -function genAssertion(email) { - var idp = JWTool.JWK.fromPEM( - generateRSAKeypair().private, - { iss: config.get('browserid.issuer') }); - var userPair = generateRSAKeypair(); - var userSecret = JWTool.JWK.fromPEM( - userPair.private, - { iss: config.get('browserid.issuer') }); - var userPublic = JWTool.JWK.fromPEM(userPair.public); - var now = Date.now(); - var cert = idp.signSync( - { - 'public-key': userPublic, - principal: { - email: email - }, - iat: now - 1000, - exp: now, - 'fxa-verifiedEmail': VEMAIL - } - ); - var assertion = userSecret.signSync( - { - aud: 'oauth.fxa', - exp: now - } - ); - - return P.resolve(cert + '~' + assertion); -} - - -// this matches the hashed secret in config, an assert sanity checks -// lower to make sure it matches -const secret = 'b93ef8a8f3e553a430d7e5b904c6132b2722633af9f03128029201d24a97f2a8'; -const secretPrevious = 'ec62e3281e3b56e702fe7e82ca7b1fa59d6c2a6766d6d28cccbf8bfa8d5fc8a8'; - -var client; -var badSecret; -var clientId; -var AN_ASSERTION; - -function authParams(params, options) { - options = options || {}; - var defaults = { - assertion: AN_ASSERTION, - client_id: options.clientId || clientId, - state: '1', - scope: 'a', - acr_values: options.acr_values || undefined - }; - - params = params || {}; - Object.keys(params).forEach(function(key) { - defaults[key] = params[key]; - }); - return defaults; -} - -function newToken(payload, options) { - payload = payload || {}; - options = options || {}; - var ttl = payload.ttl || MAX_TTL_S; - delete payload.ttl; - mockAssertion().reply(200, options.verifierResponse || VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams(payload, options) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return Server.api.post({ - url: '/token', - payload: { - client_id: options.clientId || clientId, - client_secret: options.secret || secret, - code: res.result.code, - ttl: ttl - } - }); - }); -} - -function assertInvalidRequestParam(result, param) { - assert.equal(result.code, 400); - assert.equal(result.message, 'Invalid request parameter'); - assert.equal(result.validation.keys.length, 1); - assert.equal(result.validation.keys[0], param); -} - -// helper function to create a new user, email and token for some client -/** - * - * @param {String} cId - hex client id - * @param {Object} [options] - custom options - * @param {Object} [options.uid] - custom uid - * @param {Object} [options.email] - custom email - * @param {Object} [options.scopes] - custom scopes - */ -function getUniqueUserAndToken(cId, options) { - options = options || {}; - if (! cId) { - throw new Error('No client id set'); - } - - var uid = options.uid || unique(16).toString('hex'); - var email = options.email || unique(4).toString('hex') + '@mozilla.com'; - - return db.generateAccessToken({ - clientId: buf(cId), - userId: buf(uid), - email: email, - scope: options.scopes ? ScopeSet.fromArray(options.scopes) : auth.SCOPE_CLIENT_MANAGEMENT - }).then(function (token) { - return { - uid: uid, - email: email, - token: token.token.toString('hex') - }; - }); -} - -function clientByName(name) { - return config.get('clients').reduce(function (client, lastClient) { - return client.name === name ? client : lastClient; - }); -} - -function basicAuthHeader(clientId, secret) { - return 'Basic ' + Buffer.from(clientId + ':' + secret).toString('base64'); -} - - -describe('/v1', function() { - before(function(done) { - - P.all([ - genAssertion(USERID + config.get('browserid.issuer')).then(function(ass) { - AN_ASSERTION = ass; - }), - db.ping().then(function() { - client = clientByName('Mocha'); - clientId = client.id; - assert.equal(encrypt.hash(secret).toString('hex'), client.hashedSecret); - assert.equal(encrypt.hash(secretPrevious).toString('hex'), client.hashedSecretPrevious); - badSecret = Buffer.from(secret, 'hex').slice(); - badSecret[badSecret.length - 1] ^= 1; - badSecret = badSecret.toString('hex'); - }) - ]).done(function() { done(); }, done); - }); - - afterEach(function() { - nock.cleanAll(); - }); - - describe('/authorization', function() { - - describe('GET', function() { - it('redirects with all query params', function() { - return Server.api - .get('/authorization?client_id=123&state=321&scope=1&action=signup&a=b') - .then(function(res) { - assert.equal(res.statusCode, 302); - assertSecurityHeaders(res); - var redirect = url.parse(res.headers.location, true); - - assert.equal(redirect.query.client_id, '123'); - assert.equal(redirect.query.state, '321'); - assert.equal(redirect.query.scope, '1'); - // unknown query params are forwarded - assert.equal(redirect.query.a, 'b'); - var target = url.parse(config.get('contentUrl'), true); - assert.equal(redirect.pathname, target.pathname + 'signup'); - assert.equal(redirect.host, target.host); - }); - }); - - it('redirects `action=signin` to signin', function() { - return Server.api - .get('/authorization?client_id=123&state=321&scope=1&action=signin&a=b') - .then(function(res) { - assert.equal(res.statusCode, 302); - assertSecurityHeaders(res); - var redirect = url.parse(res.headers.location, true); - - assert.equal(redirect.query.client_id, '123'); - assert.equal(redirect.query.state, '321'); - assert.equal(redirect.query.scope, '1'); - // unknown query params are forwarded - assert.equal(redirect.query.a, 'b'); - var target = url.parse(config.get('contentUrl'), true); - assert.equal(redirect.pathname, target.pathname + 'signin'); - assert.equal(redirect.host, target.host); - }); - }); - - it('redirects no action to contentUrl root', function() { - return Server.api.get('/authorization?client_id=123&state=321&scope=1') - .then(function(res) { - assert.equal(res.statusCode, 302); - assertSecurityHeaders(res); - var redirect = url.parse(res.headers.location, true); - - var target = url.parse(config.get('contentUrl'), true); - assert.equal(redirect.pathname, target.pathname); - assert.equal(redirect.host, target.host); - }); - }); - - it('redirects `action=force_auth` to force_auth', function() { - var endpoint = '/authorization?action=force_auth&email=' + - encodeURIComponent(VEMAIL); - return Server.api.get(endpoint) - .then(function(res) { - assert.equal(res.statusCode, 302); - assertSecurityHeaders(res); - var redirect = url.parse(res.headers.location, true); - - var target = url.parse(config.get('contentUrl'), true); - assert.equal(redirect.pathname, target.pathname + 'force_auth'); - assert.equal(redirect.host, target.host); - assert.equal(redirect.query.email, VEMAIL); - }); - }); - - it('redirects `action=email` to `/`', function() { - return Server.api.get('/authorization?action=email') - .then(function(res) { - assert.equal(res.statusCode, 302); - assertSecurityHeaders(res); - var redirect = url.parse(res.headers.location, true); - - var target = url.parse(config.get('contentUrl'), true); - assert.equal(redirect.pathname, target.pathname); - assert.equal(redirect.host, target.host); - assert.equal(redirect.query.action, 'email'); - }); - }); - - it('rewrites `login_hint=foo` to `email=foo`', function() { - var endpoint = '/authorization?action=signin&login_hint=' + - encodeURIComponent(VEMAIL); - return Server.api.get(endpoint) - .then(function(res) { - assert.equal(res.statusCode, 302); - assertSecurityHeaders(res); - var redirect = url.parse(res.headers.location, true); - - var target = url.parse(config.get('contentUrl'), true); - assert.equal(redirect.pathname, target.pathname + 'signin'); - assert.equal(redirect.host, target.host); - assert.equal(redirect.query.email, VEMAIL); - }); - }); - - it('should fail for invalid action', function() { - return Server.api - .get('/authorization?client_id=123&state=321&scope=1&action=something_invalid&a=b') - .then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 109); - assert.equal(res.result.validation, 'action'); - }); - }); - }); - - describe('content-type', function() { - it('should fail if unsupported', function() { - return Server.api.post({ - url: '/authorization', - headers: { - 'content-type': 'text/plain' - }, - payload: authParams() - }).then(function(res) { - assert.equal(res.statusCode, 415); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 113); - }); - }); - }); - - describe('untrusted client scope', function() { - it('should fail if invalid scopes', function() { - var client = clientByName('Untrusted'); - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client.id, - scope: 'profile:write' - }) - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 114); - assert.ok(res.result.invalidScopes.indexOf('profile:write') !== -1); - }); - }); - - it('should report all invalid scopes', function() { - var client = clientByName('Untrusted'); - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client.id, - scope: 'profile:email profile:locale profile:amr' - }) - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 114); - assert.ok(res.result.invalidScopes.indexOf('profile:email') === -1); - assert.ok(res.result.invalidScopes.indexOf('profile:locale') !== -1); - assert.ok(res.result.invalidScopes.indexOf('profile:amr') !== -1); - }); - }); - - it('should succeed if valid scope', function() { - var client = clientByName('Untrusted'); - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client.id, - scope: 'profile:email profile:uid' - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - }); - }); - - it('should succeed with https:// scopes', function() { - const scopes = 'profile:email profile:uid https://identity.mozilla.com/apps/notes https://identity.mozilla.com/apps/lockbox'; - const client = clientByName('Mocha'); - mockAssertion().reply(200, VERIFY_GOOD); - - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client.id, - scope: scopes - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - }); - }); - }); - - describe('pkce', function() { - it('should fail if Public Client is not using code_challenge', function() { - var client = clientByName('Public Client PKCE'); - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client.id, - scope: 'profile profile:write profile:uid', - }) - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 118); - assert.equal(res.result.error, 'PKCE parameters missing'); - }); - }); - - it('should allow Public Clients to direct grant without PKCE', function() { - var client = clientByName('Admin'); - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client.id, - response_type: 'token', - scope: 'profile profile:write profile:uid', - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - }); - }); - - it('only works with Public Clients', function() { - var client = clientByName('Mocha'); - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client.id, - scope: 'profile profile:write profile:uid', - response_type: 'code', - code_challenge_method: 'S256', - code_challenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM', - }) - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 116); - assert.equal(res.result.message, 'Not a public client'); - }); - }); - }); - - describe('?client_id', function() { - - it('is required', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: undefined - }) - }).then(function(res) { - assertInvalidRequestParam(res.result, 'client_id'); - assertSecurityHeaders(res); - }); - }); - - }); - - describe('?assertion', function() { - - it('is required', function() { - return Server.api.post({ - url: '/authorization', - payload: authParams({ - assertion: undefined - }) - }).then(function(res) { - assertInvalidRequestParam(res.result, 'assertion'); - assertSecurityHeaders(res); - }); - }); - - it('errors correctly if invalid', function() { - mockAssertion().reply(400, '{"status":"failure"}'); - return Server.api.post({ - url: '/authorization', - payload: authParams() - }).then(function(res) { - assert.equal(res.result.code, 401); - assert.equal(res.result.message, 'Invalid assertion'); - assertSecurityHeaders(res); - }); - }); - - it('succeeds by default when fxa-tokenVerified is false', function() { - mockAssertion().reply(200, VERIFY_GOOD_BUT_UNVERIFIED); - return Server.api.post({ - url: '/authorization', - payload: authParams() - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - }); - }); - - it('errors when fxa-tokenVerified is false and a scope has keys', function() { - mockAssertion().reply(200, VERIFY_GOOD_BUT_UNVERIFIED); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: SCOPED_CLIENT_ID, - scope: SCOPE_CAN_SCOPE_KEY - }) - }).then(function(res) { - assert.equal(res.result.code, 401); - assert.equal(res.result.message, 'Invalid assertion'); - assertSecurityHeaders(res); - }); - }); - - it('succeeds when fxa-tokenVerified is false and an unknown URL scope is requested', function() { - mockAssertion().reply(200, VERIFY_GOOD_BUT_UNVERIFIED); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: SCOPED_CLIENT_ID, - scope: 'https://example.com/unknown-scope' - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - }); - }); - - }); - - describe('?redirect_uri', function() { - it('is optional', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - redirect_uri: client.redirectUri - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert(res.result.redirect); - }); - }); - - it('must be same as registered redirect', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - redirect_uri: 'http://localhost:8080/derp' - }) - }).then(function(res) { - assert.equal(res.result.code, 400); - assert.equal(res.result.message, 'Incorrect redirect_uri'); - assertSecurityHeaders(res); - }); - }); - - describe('with config.localRedirects', function() { - beforeEach(function() { - config.set('localRedirects', true); - }); - - afterEach(function() { - config.set('localRedirects', false); - }); - - it('must be same as registered redirect with config set', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - redirect_uri: 'http://bad.uri/derp' - }) - }).then(function(res) { - assert.equal(res.result.code, 400); - assert.equal(res.result.message, 'Incorrect redirect_uri'); - assertSecurityHeaders(res); - }); - }); - - it('can be localhost with config set', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - redirect_uri: 'http://localhost:8080/derp' - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert(res.result.redirect); - }); - }); - - it('validates http and https scheme', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - redirect_uri: 'ftp://localhost:8080/derp' - }) - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 109); - assert.equal(res.result.message, 'Invalid request parameter'); - }); - }); - - it('can be 127.0.0.1 with config set', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - redirect_uri: 'http://127.0.0.1:8080/derp' - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert(res.result.redirect); - }); - }); - - }); - - it('can be a URN', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: '98e6508e88680e1b' - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - var expected = 'urn:ietf:wg:oauth:2.0:fx:webchannel'; - var actual = res.result.redirect.substr(0, expected.length); - assert.equal(actual, expected); - }); - }); - - it('can have query parameters', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: 'dcdb5ae7add825d2' - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - var expected = 'https://example.domain/return?foo=bar'; - var actual = res.result.redirect.substr(0, expected.length); - assert.equal(actual, expected); - }); - }); - }); - - describe('?state', function() { - it('is required', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - state: undefined - }) - }).then(function(res) { - assertInvalidRequestParam(res.result, 'state'); - assertSecurityHeaders(res); - }); - }); - - it('is returned', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - state: 'aa' - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.state, 'aa'); - assert.equal(url.parse(res.result.redirect, true).query.state, 'aa'); - }); - }); - }); - - describe('?scope', function() { - it('is optional', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - scope: undefined - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert(res.result.redirect); - }); - }); - - it('is restricted to expected characters', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - scope: 'profile:\u2603' - }) - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - }); - }); - }); - - describe('?response_type', function() { - it('is optional', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - response_type: undefined - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert(res.result.redirect); - }); - }); - - it('can be code', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - response_type: 'code' - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert(res.result.code); - assert(res.result.redirect); - }); - }); - - it('supports PKCE - code_challenge and code_challenge_method', function() { - var client = clientByName('Public Client PKCE'); - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client.id, - response_type: 'code', - code_challenge_method: 'S256', - code_challenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM', - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert(res.result.code); - assert(res.result.redirect); - }); - }); - - it('supports code_challenge only with code response_type', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - response_type: 'token', - code_challenge_method: 'S256', - code_challenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM', - }) - }).then(function(res) { - assert.equal(res.statusCode, 400); - assert.equal(res.result.errno, 109); - }); - }); - - it('must not be something besides code or token', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - response_type: 'foo' - }) - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - }); - }); - - it('fails if ttl is specified with code', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - response_type: 'code', - ttl: 42 - }) - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - }); - }); - - describe('token', function() { - var client2 = clientByName('Admin'); - assert(client2.canGrant); //sanity check - - it('does not require state argument', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client2.id, - state: undefined, - response_type: 'token' - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - }); - }); - - it('does not require scope argument', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client2.id, - scope: undefined, - response_type: 'token' - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - }); - }); - - it('requires a client with proper permission', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client.id, - response_type: 'token' - }) - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 110); - }); - }); - - it('returns an implicit token', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client2.id, - response_type: 'token' - }) - }).then(function(res) { - var defaultExpiresIn = config.get('expiration.accessToken') / 1000; - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert(res.result.access_token); - assert.equal(res.result.token_type, 'bearer'); - assert(res.result.scope); - assert(res.result.expires_in <= defaultExpiresIn); - assert(res.result.expires_in > defaultExpiresIn - 10); - assert(res.result.auth_at); - }); - }); - - it('honours the ttl parameter', function() { - var ttl = 42; - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client2.id, - response_type: 'token', - ttl: ttl - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert(res.result.expires_in <= ttl); - assert(res.result.expires_in > ttl - 10); - }); - }); - }); - }); - - describe('?keys_jwe', function() { - it('should validate the JWE', () => { - const keys_jwe = 'some_string'; - const code_challenge = 'iyW5ScKr22v_QL-rcW_EGlJrDSOymJvrlXlw4j7JBiQ'; - - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: clientId, - response_type: 'code', - code_challenge_method: 'S256', - code_challenge: code_challenge, - keys_jwe: keys_jwe - }) - }).then((res) => { - assert.equal(res.statusCode, 400); - assert.equal(res.result.errno, 109); - assert.equal(res.result.validation.keys[0], 'keys_jwe'); - }); - }); - - it('should return the key bundle in PKCE flow', () => { - const keys_jwe = 'MjU2R0NNIn0..8L7QykCJ5W-YZtbx.Q_8JFsdWXFNg37PCqZA_JJb4BvqAuh3UMyNE.bSOKJkZspycp9DcGRWtH6g'; - const code_verifier = 'WLjNEANMbRNUSG0uQsUZMQGgIL5FUknGz2jRipY79ZC'; - const code_challenge = 'SWac3rF5sKcyAtsXGMO9feaKqpzgCoA2zowbi20F_0c'; - const secret2 = unique.secret(); - const client2 = { - name: 'client2Public', - hashedSecret: encrypt.hash(secret2), - redirectUri: 'https://example.domain', - imageUri: 'https://example.foo.domain/logo.png', - trusted: true, - publicClient: true - }; - - return db.registerClient(client2).then(() => { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client2.id.toString('hex'), - response_type: 'code', - code_challenge_method: 'S256', - code_challenge: code_challenge, - keys_jwe: keys_jwe - }) - }).then((res) => { - assert.equal(res.statusCode, 200); - return res.result.code; - }); - }).then((code) => { - return Server.api.post({ - url: '/token', - payload: { - client_id: client2.id.toString('hex'), - code: code, - code_verifier: code_verifier - } - }); - }).then((res) => { - assert.equal(res.statusCode, 200); - assert.equal(res.result.keys_jwe, keys_jwe); - }); - - }); - }); - - describe('response', function() { - describe('with a trusted client', function() { - it('should redirect to the redirect_uri', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams() - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - var loc = url.parse(res.result.redirect, true); - var expected = url.parse(client.redirectUri, true); - assert.equal(loc.protocol, expected.protocol); - assert.equal(loc.host, expected.host); - assert.equal(loc.pathname, expected.pathname); - assert.equal(loc.query.foo, expected.query.foo); - assert(loc.query.code); - assert.equal(loc.query.code, res.result.code); - }); - }); - }); - }); - - describe('check acr payload', () => { - it('should throw error if mismatch with claims', () => { - const options = {aal: 1}; - const payload = {acr_values: 'AAL2'}; - mockAssertion().reply(200, mockVerifierResult(options)); - return Server.api.post({ - url: '/authorization', - payload: authParams(payload) - }).then(function (res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.message, 'Mismatch acr value'); - assert.equal(res.result.errno, 120, 'correct errno'); - }); - }); - - it('process request when correct acr_values in claims', () => { - const options = {aal: 2}; - const payload = {acr_values: 'AAL2'}; - mockAssertion().reply(200, mockVerifierResult(options)); - return Server.api.post({ - url: '/authorization', - payload: authParams(payload) - }).then(function (res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.ok(res.result.code, 'code set'); - assert.ok(res.result.redirect, 'redirect set'); - assert.equal(res.result.state, 1, 'correct state'); - }); - }); - }); - }); - - describe('/token', function() { - - it('disallows GET', function() { - return Server.api.get('/token').then(function(res) { - assert.equal(res.statusCode, 404); - assertSecurityHeaders(res); - }); - }); - - describe('?client_id', function() { - it('is required', function() { - return Server.api.post({ - url: '/token', - payload: { - client_secret: secret, - code: unique.code().toString('hex') - } - }).then(function(res) { - assertInvalidRequestParam(res.result, 'client_id'); - assertSecurityHeaders(res); - }); - }); - - it('is forbidden when authz header provided', function() { - return Server.api.post({ - url: '/token', - headers: { - authorization: basicAuthHeader(clientId, secret) - }, - payload: { - client_id: clientId, - client_secret: secret, - code: unique.code().toString('hex') - } - }).then(function(res) { - assertInvalidRequestParam(res.result, 'client_id'); - assertSecurityHeaders(res); - }); - }); - }); - - describe('?client_secret', function() { - it('is required', function() { - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - code: unique.code().toString('hex') - } - }).then(function(res) { - assertInvalidRequestParam(res.result, 'client_secret'); - assertSecurityHeaders(res); - }); - }); - - it('is forbidden when authz header provided', function() { - return Server.api.post({ - url: '/token', - headers: { - authorization: basicAuthHeader(clientId, secret) - }, - payload: { - client_secret: secret, - code: unique.code().toString('hex') - } - }).then(function(res) { - assertInvalidRequestParam(res.result, 'client_secret'); - assertSecurityHeaders(res); - }); - }); - - it('must match server-stored secret', function() { - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: badSecret, - code: unique.code().toString('hex') - } - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.message, 'Incorrect secret'); - }); - }); - - describe('previous secret', function() { - function getCode(clientId){ - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: clientId - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return res.result.code; - }); - } - - it('should get auth token with secret', function(){ - return getCode(clientId).then(function(code) { - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - code: code - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.ok(res.result.access_token); - assert.equal(res.result.token_type, 'bearer'); - assert.ok(res.result.auth_at); - assert.ok(res.result.expires_in); - assert.equal(res.result.scope, 'a'); - assert.equal(res.result.keys_jwe, undefined); - }); - }); - - it('should get auth token with previous secret', function(){ - return getCode(clientId).then(function(code) { - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secretPrevious, - code: code - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.ok(res.result.access_token); - }); - }); - }); - }); - - describe('authorization header', function() { - it('should allow fetching get auth token when the secret is valid', function(){ - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: clientId - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - return res.result.code; - }).then(function(code) { - return Server.api.post({ - url: '/token', - headers: { - authorization: basicAuthHeader(clientId, secret) - }, - payload: { - code: code - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.ok(res.result.access_token); - assert.equal(res.result.token_type, 'bearer'); - assert.ok(res.result.auth_at); - assert.ok(res.result.expires_in); - assert.equal(res.result.scope, 'a'); - assert.equal(res.result.keys_jwe, undefined); - }); - }); - - it('should be rejected if the secret is invalid', function() { - return Server.api.post({ - url: '/token', - headers: { - authorization: basicAuthHeader(clientId, badSecret) - }, - payload: { - code: unique.code().toString('hex') - } - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.message, 'Incorrect secret'); - }); - }); - - it('should be rejected if the credentials are malformed', function() { - return Server.api.post({ - url: '/token', - headers: { - authorization: 'Basic ' + Buffer.from('invalid').toString('base64') - }, - payload: { - code: unique.code().toString('hex') - } - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assertInvalidRequestParam(res.result, 'authorization'); - }); - }); - }); - - describe('?grant_type=authorization_code', function() { - describe('?code', function() { - it('is required', function() { - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret - } - }).then(function(res) { - assertInvalidRequestParam(res.result, 'code'); - assertSecurityHeaders(res); - }); - }); - - it('must match an existing code', function() { - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - code: unique.code().toString('hex') - } - }).then(function(res) { - assert.equal(res.result.code, 400); - assert.equal(res.result.message, 'Unknown code'); - assertSecurityHeaders(res); - }); - }); - - it('must be a code owned by this client', function() { - var secret2 = unique.secret(); - var client2 = { - name: 'client2', - hashedSecret: encrypt.hash(secret2), - redirectUri: 'https://example.domain', - imageUri: 'https://example.foo.domain/logo.png', - trusted: true - }; - return db.registerClient(client2).then(function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client2.id.toString('hex') - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return res.result.code; - }); - }).then(function(code) { - return Server.api.post({ - url: '/token', - payload: { - // client is trying to use client2's code - client_id: clientId, - client_secret: secret, - code: code - } - }); - }).then(function(res) { - assert.equal(res.result.code, 400); - assert.equal(res.result.message, 'Incorrect code'); - assertSecurityHeaders(res); - }); - - }); - - - it('consumes code via public client (PKCE)', function() { - var code_verifier = 'WFX-9dPwcpPIXt8c5Pbx09_Z61zPm1Fjwv89lVrukOh'; - var code_verifier_bad = 'QnuuNM5gfnJmWwIjiOKk2SKn8A89tph3-8BjNUUtooJ'; - var code_challenge = 'xWVKKAQVD9XSXT4Z4Oh8dLJ5pqrr0gQes2QwZOVJyAk'; - var secret2 = unique.secret(); - var oauth_code; - var client2 = { - name: 'client2Public', - hashedSecret: encrypt.hash(secret2), - redirectUri: 'https://example.domain', - imageUri: 'https://example.foo.domain/logo.png', - trusted: true, - publicClient: true - }; - return db.registerClient(client2).then(function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: client2.id.toString('hex'), - response_type: 'code', - code_challenge_method: 'S256', - code_challenge: code_challenge - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return res.result.code; - }); - }).then(function(code) { - oauth_code = code; - - return Server.api.post({ - url: '/token', - payload: { - client_id: client2.id.toString('hex'), - code: oauth_code, - code_verifier: code_verifier_bad - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 400); - assert.equal(res.result.errno, 117); - assert.equal(res.result.message, 'Incorrect code_challenge'); - }).then(function(code) { - return Server.api.post({ - url: '/token', - payload: { - client_id: client2.id.toString('hex'), - code: oauth_code, - code_verifier: code_verifier - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assert.ok(res.result.access_token); - assert.ok(res.result.scope); - assert.equal(res.result.token_type, 'bearer'); - assert.ok(res.result.access_token); - assert.equal(res.result.keys_jwe, undefined); - }); - - }); - - it('must not have expired', function() { - this.slow(200); - var exp = config.get('expiration.code'); - config.set('expiration.code', 50); - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams() - }).then(function(res) { - return res.result.code; - }).delay(60).then(function(code) { - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - code: code - } - }); - }).then(function(res) { - assert.equal(res.result.code, 400); - assert.equal(res.result.message, 'Expired code'); - assertSecurityHeaders(res); - }).finally(function() { - config.set('expiration.code', exp); - }); - }); - - it('cannot use the same code multiple times', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams() - }).then(function(res) { - return res.result.code; - }).then(function(code) { - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - code: code - } - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - code: code - } - }); - }); - }).then(function(res) { - assert.equal(res.result.code, 400); - assert.equal(res.result.message, 'Unknown code'); - assertSecurityHeaders(res); - }); - }); - }); - - describe('response', function() { - describe('access_type=online', function() { - it('should return a correct response', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - scope: 'foo bar bar' - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - code: res.result.code, - foo: 'bar' // testing stripUnknown - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.token_type, 'bearer'); - assert(res.result.access_token); - assert(! res.result.refresh_token); - assert.equal(res.result.access_token.length, - config.get('unique.token') * 2); - assert.equal(res.result.scope, 'foo bar'); - assert.equal(res.result.auth_at, AUTH_AT); - }); - }); - }); - - describe('access_type=offline', function() { - it('should return a correct response', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - scope: 'foo bar bar', - access_type: 'offline' - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - code: res.result.code, - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.token_type, 'bearer'); - assert(res.result.access_token); - assert(res.result.refresh_token); - assert.equal(res.result.access_token.length, - config.get('unique.token') * 2); - assert.equal(res.result.refresh_token.length, - config.get('unique.token') * 2); - assert.equal(res.result.scope, 'foo bar'); - assert.equal(res.result.auth_at, AUTH_AT); - }); - }); - }); - }); - - it('with a blank scope', function() { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - scope: undefined - }) - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - code: res.result.code, - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.token_type, 'bearer'); - assert(res.result.access_token); - assert.equal(res.result.access_token.length, - config.get('unique.token') * 2); - assert.equal(res.result.scope, ''); - }); - }); - - }); - - describe('?grant_type=refresh_token', function() { - - describe('?refresh_token', function() { - - it('should be required', function() { - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - grant_type: 'refresh_token' - } - }).then(function(res) { - assertInvalidRequestParam(res.result, 'refresh_token'); - assertSecurityHeaders(res); - }); - }); - - it('can refresh a token as a Public (PKCE) Client', function() { - var clientId = '38a6b9b3a65a1871'; - var clientSecret = 'd914ea58d579ec907a1a40b19fb3f3a631461fe00e494521d41c0496f49d288f'; - var refresh; - return newToken({ - access_type: 'offline', - response_type: 'code', - code_challenge_method: 'S256', - code_challenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM' - }, { - clientId: clientId, - }).then(function(res) { - assert.equal(res.statusCode, 200); - refresh = res.result.refresh_token; - assert(refresh); - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: clientSecret, - grant_type: 'refresh_token', - refresh_token: refresh - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 400, 'client_secret must not be set'); - assert.equal(res.result.errno, 109); - assert.equal(res.result.refresh_token, undefined); - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - grant_type: 'refresh_token', - refresh_token: refresh - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assert(res.result.expires_in); - assert(res.result.access_token); - assert.equal(res.result.refresh_token, undefined); - }); - }); - - it('should be an existing token', function() { - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - grant_type: 'refresh_token', - refresh_token: unique.token().toString('hex') - } - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 108); - }); - }); - - it('should be owned by the client_id', function() { - var id2; - var secret2 = unique.secret(); - var client2 = { - name: 'client2', - hashedSecret: encrypt.hash(secret2), - redirectUri: 'https://example.domain', - imageUri: 'https://example.foo.domain/logo.png', - trusted: true - }; - return db.registerClient(client2).then(function(c) { - id2 = c.id.toString('hex'); - return newToken({ access_type: 'offline' }); //for main client - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - var refresh = res.result.refresh_token; - assert(refresh); - return Server.api.post({ - url: '/token', - payload: { - client_id: id2, // client2 stole it somehow - client_secret: secret2.toString('hex'), - grant_type: 'refresh_token', - refresh_token: refresh - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 108, 'invalid token'); - }); - }); - - it('should not create a new refresh token', function() { - return newToken({ access_type: 'offline' }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - grant_type: 'refresh_token', - refresh_token: res.result.refresh_token - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.refresh_token, undefined); - }); - }); - - }); - - describe('?scope', function() { - - it('should be able to reduce scopes', function() { - return newToken({ - access_type: 'offline', - scope: 'foo bar:baz' - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.scope, 'foo bar:baz'); - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - grant_type: 'refresh_token', - refresh_token: res.result.refresh_token, - scope: 'foo' - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.scope, 'foo'); - }); - }); - - it('should not expand scopes', function() { - return newToken({ - access_type: 'offline', - scope: 'foo bar:baz' - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.scope, 'foo bar:baz'); - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - grant_type: 'refresh_token', - refresh_token: res.result.refresh_token, - scope: 'foo quux' - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 114); - }); - }); - - it('should not expand read scope to write scope', function() { - return newToken({ - access_type: 'offline', - scope: 'foo' - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.scope, 'foo'); - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - grant_type: 'refresh_token', - refresh_token: res.result.refresh_token, - scope: 'foo:write' - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 114); - }); - }); - - }); - - describe('?ttl', function() { - - it('should reduce the expires_in of the access_token', function() { - return newToken({ access_type: 'offline' }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - grant_type: 'refresh_token', - refresh_token: res.result.refresh_token, - ttl: 60 - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert(res.result.expires_in <= 60); - }); - }); - - it('should not exceed the maximum', function() { - return newToken({ access_type: 'offline' }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - grant_type: 'refresh_token', - refresh_token: res.result.refresh_token, - ttl: MAX_TTL_S * 100 - } - }); - }).then(function(res) { - assertInvalidRequestParam(res.result, 'ttl'); - assertSecurityHeaders(res); - }); - }); - - }); - - }); - - describe('?grant_type=jwt', function() { - const JWT_URN = 'urn:ietf:params:oauth:grant-type:jwt-bearer'; - const JKU = config.get('serviceClients')[0].jku; - assert.equal(config.get('serviceClients')[0].scope, 'profile', - 'test service client scope sanity check'); - - function sign(payload) { - return JWTool.sign({ - header: { - alg: 'RS256', - typ: 'JWT', - jku: JKU, - kid: 'dev-1' - }, - payload: { - sub: payload.sub || USERID, - iat: Math.floor(payload.iat || (Date.now() / 1000)), - exp: Math.floor(payload.exp || (Date.now() / 1000 + 60)), - aud: payload.aud || (config.get('publicUrl') + '/v1/token'), - scope: payload.scope || 'profile' - } - }, JWT_PRIV_KEY.pem); - } - - function mockJwt() { - var parts = url.parse(JKU); - nock(parts.protocol + '//' + parts.host).get(parts.path) - .reply(200, { - keys: [ - JWT_PUB_KEY - ] - }); - } - - function request(payload) { - var assertion = sign(payload); - mockJwt(); - - return Server.api.post({ - url: '/token', - payload: { - grant_type: JWT_URN, - assertion: assertion - } - }); - } - - describe('response', function() { - it('should work', function() { - return request({ - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - }); - }); - }); - - describe('userid', function() { - it('should fail if invalid', function() { - return request({ - sub: 'definitely not an fxa uid', - }).then(function(res) { - assert.equal(res.statusCode, 401); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 104); - }); - }); - }); - - describe('audience', function() { - it('should fail if mismatch', function() { - return request({ - aud: 'https://not.the.right.aud/ience', - }).then(function(res) { - assert.equal(res.statusCode, 401); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 104); - }); - }); - }); - - describe('issuedat', function() { - it('should fail if in the future', function() { - return request({ - iat: 60 + Math.floor(Date.now() / 1000) - }).then(function(res) { - assert.equal(res.statusCode, 401); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 104); - }); - }); - }); - - describe('expiresat', function() { - it('should fail if in the past', function() { - return request({ - exp: Math.floor(Date.now() / 1000) - 100 - }).then(function(res) { - assert.equal(res.statusCode, 401); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 104); - }); - }); - }); - - describe('scope', function() { - it('should be able to reduce scopes', function() { - return request({ - scope: 'profile:email' - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.scope, 'profile:email'); - }); - }); - - it('should not be able to increase scopes', function() { - return request({ - scope: 'nuclear:codes' - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 114); - }); - }); - }); - }); - - describe('?scope=openid', function() { - - function decodeJWT(b64) { - var jwt = b64.split('.'); - return { - header: JSON.parse(Buffer.from(jwt[0], 'base64').toString('utf-8')), - claims: JSON.parse(Buffer.from(jwt[1], 'base64').toString('utf-8')) - }; - } - - it('should return an id_token', () => { - return newToken({ scope: 'openid' }).then(res => { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert(res.result.access_token); - assert(res.result.id_token); - const jwt = decodeJWT(res.result.id_token); - const header = jwt.header; - const claims = jwt.claims; - - assert.equal(header.alg, 'RS256'); - assert.equal(header.kid, config.get('openid.key').kid); - - assert.equal(claims.sub, USERID); - assert.equal(claims.aud, clientId); - assert.equal(claims.iss, config.get('openid.issuer')); - const now = Math.floor(Date.now() / 1000); - assert(claims.iat <= now); - assert(claims.exp > now); - assert.deepEqual(claims.amr, AMR); - assert.equal(claims.acr, ACR); - assert.equal(claims['fxa-aal'], AAL); - - const at_hash = util.generateTokenHash(Buffer.from(res.result.access_token, 'hex')); - assert.equal(claims.at_hash, at_hash); - }); - }); - - it('should omit amr claim when not given in the assertion', () => { - let verifierResponse = JSON.parse(VERIFY_GOOD); - delete verifierResponse.idpClaims['fxa-amr']; - verifierResponse = JSON.stringify(verifierResponse); - return newToken({ scope: 'openid' }, { verifierResponse }).then(res => { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert(res.result.id_token); - const jwt = decodeJWT(res.result.id_token); - const claims = jwt.claims; - - assert.equal(claims.sub, USERID); - assert.equal(claims.aud, clientId); - assert.equal(claims.iss, config.get('openid.issuer')); - assert.equal(claims.amr, undefined); - assert.equal(claims.acr, ACR); - assert.equal(claims['fxa-aal'], AAL); - }); - }); - - it('should omit acr and fxa-aal claims when not given in the assertion', () => { - let verifierResponse = JSON.parse(VERIFY_GOOD); - delete verifierResponse.idpClaims['fxa-aal']; - verifierResponse = JSON.stringify(verifierResponse); - return newToken({ scope: 'openid' }, { verifierResponse }).then(res => { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert(res.result.id_token); - const jwt = decodeJWT(res.result.id_token); - const claims = jwt.claims; - - assert.equal(claims.sub, USERID); - assert.equal(claims.aud, clientId); - assert.equal(claims.iss, config.get('openid.issuer')); - assert.deepEqual(claims.amr, AMR); - assert.equal(claims.acr, undefined); - assert.equal(claims['fxa-aal'], undefined); - }); - }); - - it('should be available to untrusted reliers', function() { - const client = clientByName('Untrusted'); - return newToken({ scope: 'openid' }, { client_id: client.id }).then(res => { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert(res.result.access_token); - assert(res.result.id_token); - }); - }); - - }); - - describe('?redirect_uri', () => { - function getCode(clientId) { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - client_id: clientId - }) - }).then((res) => { - return res.result.code; - }); - } - it('works with https redirect_uri', () => { - return getCode(clientId).then((code) => { - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - code: code, - redirect_uri: 'https://2aa95473a5115d5f3deb36bb6875cf76f05e4c4d.extensions.allizom.org/' - } - }); - }).then((res) => { - assert.equal(res.statusCode, 200); - }); - }); - - it('works with app redirect_uri', () => { - return getCode(clientId).then((code) => { - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - code: code, - redirect_uri: 'testpilot-notes://redirect.android' - } - }); - }).then((res) => { - assert.equal(res.statusCode, 200); - }); - }); - - it('works with query parameters', () => { - return getCode(clientId).then((code) => { - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - code: code, - redirect_uri: 'https://example.com?extra=params&go=here' - } - }); - }).then((res) => { - assert.equal(res.statusCode, 200); - }); - }); - - it('is validated', () => { - return getCode(clientId).then((code) => { - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - code: code, - redirect_uri: 'https://foo\n\n<>\n\r' - } - }); - }).then((res) => { - assert.equal(res.statusCode, 400); - assertInvalidRequestParam(res.result, 'redirect_uri'); - assertSecurityHeaders(res); - }); - }); - - }); - - }); - - describe('/client', function() { - var clientName = 'test/api/client'; - var clientUri = 'http://test.api/client'; - - var tok; - var badTok; - - before(function() { - return db.generateAccessToken({ - clientId: buf(clientId), - userId: buf(USERID), - email: VEMAIL, - scope: auth.SCOPE_CLIENT_MANAGEMENT - }).then(function(token) { - tok = token.token.toString('hex'); - return db.generateAccessToken({ - clientId: buf(clientId), - userId: unique(16), - email: 'user@not.allow.ed', - scope: auth.SCOPE_CLIENT_MANAGEMENT - }); - }).then(function(token) { - badTok = token.token.toString('hex'); - }); - }); - - describe('GET /:id', function() { - describe('response', function() { - it('should return the correct response', function() { - return Server.api.get('/client/' + clientId) - .then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - var body = res.result; - assert.equal(body.name, client.name); - assert(body.image_uri); - assert(body.redirect_uri); - assert(body.trusted); - }); - }); - }); - - it('should allow for clients with no redirect_uri', function() { - return Server.api.get('/client/ea3ca969f8c6bb0d') - .then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - var body = res.result; - assert(body.name); - assert.equal(body.image_uri, ''); - assert.equal(body.redirect_uri, ''); - }); - }); - }); - - describe('client management api', function() { - it('should not be available on main server', function(){ - return P.all([ - Server.api.get('/clients'), - Server.api.post('/client'), - Server.api.post('/client/' + clientId), - Server.api.delete('/client/' + clientId) - ]).map(function(res) { - assert.equal(res.statusCode, 404); - assertSecurityHeaders(res); - }); - }); - - describe('GET /client/:id', function() { - describe('response', function() { - it('should support the client id path', function() { - return Server.internal.api.get('/client/' + clientId) - .then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - var body = res.result; - assert.equal(body.name, client.name); - assert(body.image_uri); - assert(body.redirect_uri); - }); - }); - }); - }); - - describe('GET /clients', function() { - it('should require authorization', function() { - return Server.internal.api.get({ - url: '/clients' - }).then(function(res) { - assert.equal(res.statusCode, 401); - assertSecurityHeaders(res); - }); - }); - - it('should check whether the user is allowed', function() { - return Server.internal.api.get({ - url: '/clients', - headers: { - authorization: 'Bearer ' + badTok - } - }).then(function(res) { - assert.equal(res.statusCode, 403); - assertSecurityHeaders(res); - }); - }); - - it('should return an empty list of clients', function() { - // this developer has no clients associated, it returns 0 - // value is the same as the API endpoint and a DB call - - return Server.internal.api.get({ - url: '/clients', - headers: { - authorization: 'Bearer ' + tok - } - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - - return db.getClients(VEMAIL).then(function(clients) { - assert.equal(res.result.clients.length, clients.length); - assert.equal(res.result.clients.length, 0); - }); - }); - }); - - it('should return a list of clients for a developer', function() { - var vemail, tok; - - return getUniqueUserAndToken(clientId) - .then(function(data) { - tok = data.token; - vemail = data.email; - // make this user a developer - return db.activateDeveloper(vemail); - }).then(function() { - return db.getDeveloper(vemail); - }).then(function(developer) { - var devId = developer.developerId; - return db.registerClientDeveloper(devId, clientId); - }).then(function () { - return Server.internal.api.get({ - url: '/clients', - headers: { - authorization: 'Bearer ' + tok - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return db.getClients(vemail).then(function(clients) { - assert.equal(res.result.clients.length, clients.length); - assert.equal(res.result.clients.length, 1); - }); - }); - }); - }); - - describe('POST', function() { - before(function() { - return Server.internal.api.post({ - url: '/developer/activate', - headers: { - authorization: 'Bearer ' + tok - } - }).then(function(res) { - }); - }); - - it('should register a client', function() { - return Server.internal.api.post({ - url: '/client', - headers: { - authorization: 'Bearer ' + tok, - }, - payload: { - name: clientName, - redirect_uri: clientUri, - image_uri: clientUri + '/image', - can_grant: true, - trusted: true - } - }).then(function(res) { - assert.equal(res.statusCode, 201); - assertSecurityHeaders(res); - var client = res.result; - assert(client.id); - return db.getClient(client.id).then(function(klient) { - assert.equal(klient.id.toString('hex'), client.id); - assert.equal(klient.name, client.name); - assert.equal(klient.redirectUri, client.redirect_uri); - assert.equal(klient.imageUri, client.image_uri); - assert.equal(klient.redirectUri, clientUri); - assert.equal(klient.imageUri, clientUri + '/image'); - assert.equal(klient.canGrant, true); - assert.equal(klient.trusted, true); - }); - }); - }); - - it('should require authorization', function() { - return Server.internal.api.post({ - url: '/client', - payload: { - name: 'dont matter' - } - }).then(function(res) { - assert.equal(res.statusCode, 401); - assertSecurityHeaders(res); - }); - }); - - it('should check the whether the user is allowed', function() { - return Server.internal.api.post({ - url: '/client', - headers: { - authorization: 'Bearer ' + badTok - } - }).then(function(res) { - assert.equal(res.statusCode, 403); - assertSecurityHeaders(res); - }); - }); - - it('should default optional fields to sensible values', function() { - return Server.internal.api.post({ - url: '/client', - headers: { - authorization: 'Bearer ' + tok, - }, - payload: { - name: clientName, - redirect_uri: clientUri - } - }).then(function(res) { - assert.equal(res.statusCode, 201); - assertSecurityHeaders(res); - var client = res.result; - assert(client.id); - assert(client.image_uri === ''); - assert(client.can_grant === false); - assert(client.trusted === false); - return db.getClient(client.id).then(function(klient) { - assert.equal(klient.id.toString('hex'), client.id); - assert.equal(klient.name, client.name); - assert.equal(klient.imageUri, ''); - assert.equal(klient.canGrant, false); - assert.equal(klient.trusted, false); - }); - }); - }); - }); - - describe('POST /:id', function() { - var id = unique.id(); - - it('should forbid update to unknown developers', function() { - var vemail, tok; - var id = unique.id(); - var client = { - name: 'test/api/update', - id: id, - hashedSecret: encrypt.hash(unique.secret()), - redirectUri: 'https://example.domain', - imageUri: 'https://example.com/logo.png', - trusted: true - }; - - return db.registerClient(client) - .then(function () { - return getUniqueUserAndToken(id.toString('hex')); - }) - .then(function(data) { - tok = data.token; - vemail = data.email; - - return db.activateDeveloper(vemail); - }).then(function () { - return db.getDeveloper(vemail); - }).then(function (developer) { - }).then(function () { - return Server.internal.api.post({ - url: '/client/' + id.toString('hex'), - headers: { - authorization: 'Bearer ' + tok, - }, - payload: { - name: 'updated', - redirect_uri: clientUri - } - }); - }).then(function (res) { - assert.equal(res.statusCode, 401); - assertSecurityHeaders(res); - }); - }); - - it('should allow client update', function() { - var vemail, tok, devId; - var id = unique.id(); - var client = { - name: 'test/api/update2', - id: id, - hashedSecret: encrypt.hash(unique.secret()), - redirectUri: 'https://example.domain', - imageUri: 'https://example.com/logo.png', - trusted: true - }; - - return db.registerClient(client) - .then(function () { - return getUniqueUserAndToken(id.toString('hex')); - }) - .then(function(data) { - tok = data.token; - vemail = data.email; - - return db.activateDeveloper(vemail); - }).then(function () { - return db.getDeveloper(vemail); - }).then(function (developer) { - devId = developer.developerId; - }).then(function () { - return db.registerClientDeveloper( - devId.toString('hex'), - id.toString('hex') - ); - }).then(function () { - return Server.internal.api.post({ - url: '/client/' + id.toString('hex'), - headers: { - authorization: 'Bearer ' + tok, - }, - payload: { - name: 'updated', - redirect_uri: clientUri - } - }); - }).then(function (res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.payload, '{}'); - return db.getClient(client.id); - }).then(function (klient) { - assert.equal(klient.name, 'updated'); - assert.equal(klient.redirectUri, clientUri); - assert.equal(klient.imageUri, client.imageUri); - assert.equal(klient.trusted, true); - assert.equal(klient.canGrant, false); - }); - }); - - it('should forbid unknown properties', function () { - return Server.internal.api.post({ - url: '/client/' + id.toString('hex'), - headers: { - authorization: 'Bearer ' + tok - }, - payload: { - foo: 'bar' - } - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - }); - }); - - it('should require authorization', function() { - return Server.internal.api.post({ - url: '/client/' + id.toString('hex'), - payload: { - name: 'dont matter' - } - }).then(function(res) { - assert.equal(res.statusCode, 401); - assertSecurityHeaders(res); - }); - }); - - it('should check the whether the user is allowed', function() { - return Server.internal.api.post({ - url: '/client/' + id.toString('hex'), - headers: { - authorization: 'Bearer ' + badTok - } - }).then(function(res) { - assert.equal(res.statusCode, 403); - assertSecurityHeaders(res); - }); - }); - }); - - describe('DELETE /:id', function() { - - it('should delete the client', function() { - var vemail, tok, devId; - var id = unique.id(); - var client = { - name: 'test/api/deleteOwner', - id: id, - hashedSecret: encrypt.hash(unique.secret()), - redirectUri: 'https://example.domain', - imageUri: 'https://example.com/logo.png', - trusted: true - }; - - return db.registerClient(client) - .then(function () { - return getUniqueUserAndToken(id.toString('hex')); - }) - .then(function(data) { - tok = data.token; - vemail = data.email; - - return db.activateDeveloper(vemail); - }).then(function () { - return db.getDeveloper(vemail); - }).then(function (developer) { - devId = developer.developerId; - }).then(function () { - return db.registerClientDeveloper( - devId.toString('hex'), - id.toString('hex') - ); - }).then(function () { - return Server.internal.api.delete({ - url: '/client/' + id.toString('hex'), - headers: { - authorization: 'Bearer ' + tok, - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 204); - assertSecurityHeaders(res); - return db.getClient(id); - }).then(function(client) { - assert.equal(client, undefined); - }); - }); - - it('should not delete the client if not owner', function() { - var vemail, tok; - var id = unique.id(); - var client = { - name: 'test/api/deleteOwner', - id: id, - hashedSecret: encrypt.hash(unique.secret()), - redirectUri: 'https://example.domain', - imageUri: 'https://example.com/logo.png', - trusted: true - }; - - return db.registerClient(client) - .then(function () { - return getUniqueUserAndToken(id.toString('hex')); - }) - .then(function(data) { - tok = data.token; - vemail = data.email; - - return db.activateDeveloper(vemail); - }).then(function () { - return db.getDeveloper(vemail); - }).then(function (developer) { - }).then(function () { - return Server.internal.api.delete({ - url: '/client/' + id.toString('hex'), - headers: { - authorization: 'Bearer ' + tok, - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 401); - assertSecurityHeaders(res); - return db.getClient(id); - }).then(function(klient) { - assert.equal(klient.id.toString('hex'), id.toString('hex')); - }); - }); - - it('should require authorization', function() { - var id = unique.id(); - - return Server.internal.api.delete({ - url: '/client/' + id.toString('hex'), - payload: { - name: 'dont matter' - } - }).then(function(res) { - assert.equal(res.statusCode, 401); - assertSecurityHeaders(res); - }); - }); - - it('should check the whether the user is allowed', function() { - var id = unique.id(); - - return Server.internal.api.delete({ - url: '/client/' + id.toString('hex'), - headers: { - authorization: 'Bearer ' + badTok - } - }).then(function(res) { - assert.equal(res.statusCode, 403); - assertSecurityHeaders(res); - }); - }); - }); - }); - - describe('POST /key-data', function() { - let genericRequest; - - beforeEach(function () { - genericRequest = { - url: '/key-data', - payload: { - assertion: AN_ASSERTION, - client_id: SCOPED_CLIENT_ID, - scope: SCOPE_CAN_SCOPE_KEY - } - }; - }); - - it('works with a correct response', () => { - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post(genericRequest) - .then((res) => { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(Object.keys(res.result).length, 1, 'only one scope returned'); - - const body = res.result[SCOPE_CAN_SCOPE_KEY]; - - assert.equal(body.identifier, 'https://identity.mozilla.com/apps/sample-scope-can-scope-key'); - assert.equal(body.keyRotationSecret, '0000000000000000000000000000000000000000000000000000000000000000'); - assert.equal(body.keyRotationTimestamp, 123456); - }); - }); - - it('works with multiple scopes', () => { - mockAssertion().reply(200, VERIFY_GOOD); - const ANOTHER_CAN_SCOPE_KEY = 'https://identity.mozilla.com/apps/another-can-scope-key'; - genericRequest.payload.scope = `${SCOPE_CAN_SCOPE_KEY} ${ANOTHER_CAN_SCOPE_KEY}`; - - return Server.api.post(genericRequest) - .then((res) => { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(Object.keys(res.result).length, 2, 'two scopes returned'); - - const keyOne = res.result[SCOPE_CAN_SCOPE_KEY]; - const keyTwo = res.result[ANOTHER_CAN_SCOPE_KEY]; - - assert.equal(keyOne.identifier, SCOPE_CAN_SCOPE_KEY); - assert.equal(keyOne.keyRotationSecret, '0000000000000000000000000000000000000000000000000000000000000000'); - assert.equal(keyOne.keyRotationTimestamp, 123456); - - assert.equal(keyTwo.identifier, ANOTHER_CAN_SCOPE_KEY); - assert.equal(keyTwo.keyRotationSecret, '0000000000000000000000000000000000000000000000000000000000000000'); - assert.equal(keyTwo.keyRotationTimestamp, 123456); - }); - }); - - it('fails with non-existent client_id', () => { - genericRequest.payload.client_id = BAD_CLIENT_ID; - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post(genericRequest) - .then((res) => { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - const body = res.result; - assert.equal(body.errno, 101); - assert.equal(body.message, 'Unknown client'); - }); - }); - - it('succeeds with a non-scoped-key scope', () => { - genericRequest.payload.scope = 'https://identity.mozilla.com/apps/sample-scope'; - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post(genericRequest) - .then((res) => { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(Object.keys(res.result).length, 0, 'no scoped keys'); - }); - }); - - it('succeeds with scopes that arent explicitly defined in config', () => { - genericRequest.payload.scope += ' kv'; - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post(genericRequest) - .then((res) => { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.deepEqual(Object.keys(res.result), [SCOPE_CAN_SCOPE_KEY], 'undefined scope is ignored'); - }); - }); - - it('fails with bad assertion', () => { - return Server.api.post(genericRequest) - .then((res) => { - assert.equal(res.statusCode, 401); - assertSecurityHeaders(res); - const body = res.result; - assert.equal(body.message, 'Invalid assertion'); - }); - }); - - it('fails for clients that do not have the scope', () => { - genericRequest.payload.client_id = NO_KEY_SCOPES_CLIENT_ID; - - mockAssertion().reply(200, VERIFY_GOOD); - return Server.api.post(genericRequest) - .then((res) => { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(Object.keys(res.result).length, 0, 'no scoped keys'); - }); - }); - - it('fails for assertions with lastAuthAt too far in the past', () => { - genericRequest.payload.client_id = NO_KEY_SCOPES_CLIENT_ID; - - mockAssertion().reply(200, VERIFY_GOOD_BUT_STALE); - return Server.api.post(genericRequest) - .then((res) => { - assert.equal(res.statusCode, 401); - assertSecurityHeaders(res); - const body = res.result; - assert.equal(body.errno, 119); - assert.equal(body.message, 'Stale authentication timestamp'); - }); - }); - }); - - }); - - describe('/developer', function() { - describe('POST /developer/activate', function() { - it('should create a developer', function() { - var vemail, tok; - - return getUniqueUserAndToken(clientId) - .then(function(data) { - tok = data.token; - vemail = data.email; - - return db.getDeveloper(vemail); - }).then(function(developer) { - assert.equal(developer, null); - - return Server.internal.api.post({ - url: '/developer/activate', - headers: { - authorization: 'Bearer ' + tok - } - }); - - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.email, vemail); - assert(res.result.developerId); - assert(res.result.createdAt); - - return db.getDeveloper(vemail); - }).then(function(developer) { - - assert.equal(developer.email, vemail); - }); - }); - }); - - describe('GET /developer', function() { - it('should not exist', function() { - return Server.internal.api.get('/developer') - .then(function(res) { - assert.equal(res.statusCode, 404); - assertSecurityHeaders(res); - }); - }); - }); - - }); - - describe('/verify', function() { - - describe('unknown token', function() { - it('should not error', function() { - return Server.api.post({ - url: '/verify', - payload: { - token: unique.token().toString('hex') - } - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - }); - }); - }); - - it('should reject expired tokens from after the epoch', function() { - this.slow(2200); - var epoch = config.get('expiration.accessTokenExpiryEpoch'); - config.set('expiration.accessTokenExpiryEpoch', Date.now()); - return newToken({ - ttl: 1 - }).delay(1500).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.expires_in, 1); - return Server.api.post({ - url: '/verify', - payload: { - token: res.result.access_token - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 115); - }).finally(function() { - config.set('expiration.accessTokenExpiryEpoch', epoch); - }); - }); - - it('should accept expired tokens from before the epoch', function() { - this.slow(2200); - var epoch = config.get('expiration.accessTokenExpiryEpoch'); - config.set('expiration.accessTokenExpiryEpoch', Date.now() + 2000); - return newToken({ - ttl: 1 - }).delay(1500).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.expires_in, 1); - return Server.api.post({ - url: '/verify', - payload: { - token: res.result.access_token - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - }).finally(function() { - config.set('expiration.accessTokenExpiryEpoch', epoch); - }); - }); - - describe('response', function() { - it('should return the correct response', function () { - return newToken({scope: 'profile'}).then(function (res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return Server.api.post({ - url: '/verify', - payload: { - token: res.result.access_token - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.user, USERID); - assert.equal(res.result.client_id, clientId); - assert.equal(res.result.scope[0], 'profile'); - assert.equal(res.result.email, undefined); - assert.equal(res.result.profile_changed_at, undefined); - }); - }); - - it('should return profile_changed_at when set', function () { - const verifierResponse = mockVerifierResult({profileChangedAt: PROFILE_CHANGED_AT_LATER_TIME}); - return newToken({scope: 'profile'}, {verifierResponse}).then(function (res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return Server.api.post({ - url: '/verify', - payload: { - token: res.result.access_token - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.user, USERID); - assert.equal(res.result.client_id, clientId); - assert.equal(res.result.scope[0], 'profile'); - assert.equal(res.result.email, undefined); - assert.equal(res.result.profile_changed_at, PROFILE_CHANGED_AT_LATER_TIME, 'profile changed at is correct'); - }); - }); - }); - - it('should not return the email by default for profile:email scope', function() { - return newToken({ scope: 'profile:email' }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return Server.api.post({ - url: '/verify', - payload: { - token: res.result.access_token - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.email, undefined); - }); - }); - - it('should not return email for payload having email:false', function() { - return newToken({ scope: 'profile:email' }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return Server.api.post({ - url: '/verify', - payload: { - token: res.result.access_token, - email: false - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.email, undefined); - }); - }); - - it('should not return email for payload having email:true', function() { - return newToken({ scope: 'profile:email' }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - return Server.api.post({ - url: '/verify', - payload: { - token: res.result.access_token, - email: true - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.equal(res.result.email, undefined); - }); - }); - - }); - - describe('/destroy', function() { - it('should destroy access tokens', function() { - var token; - return newToken().then(function(res) { - token = res.result.access_token; - return Server.api.post({ - url: '/destroy', - payload: { - token: token - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.deepEqual(res.result, {}); - return db.getAccessToken(encrypt.hash(token)).then(function(tok) { - assert.equal(tok, undefined); - }); - }); - }); - - it('should destroy refresh tokens', function() { - var token; - return newToken({ access_type: 'offline' }).then(function(res) { - token = res.result.refresh_token; - return Server.api.post({ - url: '/destroy', - payload: { - refresh_token: token - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert.deepEqual(res.result, {}); - return db.getRefreshToken(encrypt.hash(token)).then(function(tok) { - assert.equal(tok, undefined); - }); - }); - }); - it('should accept client_secret', function() { - return newToken().then(function(res) { - return Server.api.post({ - url: '/destroy', - payload: { - token: res.result.access_token, - client_secret: 'foo' - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - }); - }); - it('should accept empty client_secret', function() { - return newToken().then(function(res) { - return Server.api.post({ - url: '/destroy', - payload: { - token: res.result.access_token, - client_secret: '' - } - }); - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - }); - }); - }); - - describe('/jwks', function() { - it('should not include the private part of the key', function() { - return Server.api.get({ - url: '/jwks' - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - - var key = res.result.keys[0]; - assert(key.n); - assert(key.e); - assert(! key.d); - }); - }); - - it('should include the oldKey if present', function() { - return Server.api.get({ - url: '/jwks' - }).then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - - var keys = res.result.keys; - assert.equal(keys.length, 2); - assert(! keys[1].d); - assert.notEqual(keys[0].kid, keys[1].kid); - }); - }); - }); - - describe('/client-tokens', function() { - var BAD_TOKEN = '0000000000000000000000000000000000000000000000000000000000000000'; - var tokenWithClientWrite; - var tokenWithoutClientWrite; - var user1; - var user2; - var client1Id; - var client2Id; - var client1; - var client2; - - beforeEach(function () { - user1 = { - uid: unique(16).toString('hex'), - email: unique(10).toString('hex') + '@token.city' - }; - - user2 = { - uid: unique(16).toString('hex'), - email: unique(10).toString('hex') + '@token.city' - }; - - client1Id = unique.id(); - client1 = { - name: 'test/api/client-tokens/list-b', - id: client1Id, - hashedSecret: encrypt.hash(unique.secret()), - redirectUri: 'https://example.domain', - imageUri: 'https://example.com/logo.png', - trusted: true - }; - - client2Id = unique.id(); - client2 = { - name: 'test/api/client-tokens/list-a', - id: client2Id, - hashedSecret: encrypt.hash(unique.secret()), - redirectUri: 'https://example.domain', - imageUri: 'https://example.com/logo.png', - trusted: false - }; - - // create a new client - return db.registerClient(client1) - .then(function () { - // user1 gets a client write token - return getUniqueUserAndToken(client1Id.toString('hex'), { - uid: user1.uid, - email: user1.email, - scopes: ['profile', 'clients:write'] - }); - }) - .then(function (result) { - tokenWithClientWrite = result.token; - }); - }); - - describe('GET /client-tokens', function() { - - it('should list connected services in set order', function() { - return db.registerClient(client2) - .then(function () { - return getUniqueUserAndToken(client2Id.toString('hex'), { - uid: user1.uid, - email: user1.email, - scopes: ['profile'] - }); - }) - .then(function (result) { - tokenWithoutClientWrite = result.token; - - return Server.api.get({ - url: '/client-tokens', - headers: { - authorization: 'Bearer ' + tokenWithoutClientWrite - } - }); - }) - .then(function (res) { - var result = res.result; - assert.equal(result.code, 403, 'list does not fetch without a proper token'); - assert.equal(result.error, 'Forbidden'); - assertSecurityHeaders(res); - - return Server.api.get({ - url: '/client-tokens', - headers: { - authorization: 'Bearer ' + tokenWithClientWrite - } - }); - }) - .then(function (res) { - assert.equal(res.statusCode, 200); - // The API sorts the results by createdAt and then by name - // The precision is one second, this test guarantees that if - // the tokens were created in the same second, they will still be sorted by name. - var result = res.result; - assert.equal(result.length, 2); - assert.equal(result[0].id, client2Id.toString('hex')); - assert.ok(result[0].lastAccessTime); - assert.equal(result[0].lastAccessTimeFormatted, 'a few seconds ago'); - assert.equal(result[0].name, 'test/api/client-tokens/list-a'); - - assert.equal(result[1].id, client1Id.toString('hex')); - assert.ok(result[1].lastAccessTime); - assert.equal(result[1].lastAccessTimeFormatted, 'a few seconds ago'); - assert.equal(result[1].name, 'test/api/client-tokens/list-b'); - assertSecurityHeaders(res); - }); - }); - - it('should not list tokens of different users', function() { - return db.registerClient(client2) - .then(function () { - return getUniqueUserAndToken(client2Id.toString('hex'), { - uid: user2.uid, - email: user2.email, - scopes: ['profile'] - }); - }) - .then(function (res) { - return Server.api.get({ - url: '/client-tokens', - headers: { - authorization: 'Bearer ' + tokenWithClientWrite - } - }); - }) - .then(function (res) { - var result = res.result; - assert.equal(result.length, 1); - assert.equal(result[0].id, client1Id.toString('hex')); - assert.equal(result[0].lastAccessTimeFormatted, 'a few seconds ago'); - assert.equal(result[0].name, 'test/api/client-tokens/list-b'); - assertSecurityHeaders(res); - }); - }); - - it('should not list canGrant=1 clients', function() { - return db.registerClient({ - name: 'test/api/client-tokens/list-can-grant', - id: client2Id, - hashedSecret: encrypt.hash(unique.secret()), - redirectUri: 'https://example.domain', - imageUri: 'https://example.com/logo.png', - trusted: true, - canGrant: true - }) - .then(function () { - return getUniqueUserAndToken(client2Id.toString('hex'), { - uid: user1.uid, - email: user1.email, - scopes: ['profile'] - }); - }) - .then(function (res) { - return Server.api.get({ - url: '/client-tokens', - headers: { - authorization: 'Bearer ' + tokenWithClientWrite - } - }); - }) - .then(function (res) { - var result = res.result; - assert.equal(result.length, 1); - assert.equal(result[0].id, client1Id.toString('hex')); - assertSecurityHeaders(res); - }); - }); - - it('should only list one client for multiple tokens', function() { - var tok; - return getUniqueUserAndToken(client1Id.toString('hex'), { - uid: user1.uid, - email: user1.email, - scopes: ['profile', 'profile:write'] - }) - .then(function () { - return getUniqueUserAndToken(client1Id.toString('hex'), { - uid: user1.uid, - email: user1.email, - scopes: ['clients:write'] - }); - }) - .then(function () { - return getUniqueUserAndToken(client1Id.toString('hex'), { - uid: user1.uid, - email: user1.email, - scopes: ['profile'] - }); - }) - .then(function (client) { - return db.getAccessToken(encrypt.hash(client.token)); - }) - .then(function (token) { - tok = token; - return Server.api.get({ - url: '/client-tokens', - headers: { - authorization: 'Bearer ' + tokenWithClientWrite - } - }); - }) - .then(function (res) { - var result = res.result; - assert.equal(result.length, 1); - assert.equal(result[0].id, client1Id.toString('hex')); - assert.equal(result[0].lastAccessTime, tok.createdAt.getTime(), 'lastAccessTime should be equal to the latest Token createdAt time'); - assertSecurityHeaders(res); - assert.deepEqual(result[0].scope, ['clients:write', 'profile:write']); - }); - }); - - it('should only return union of scopes for multiple tokens', function() { - return getUniqueUserAndToken(client1Id.toString('hex'), { - uid: user1.uid, - email: user1.email, - scopes: ['profile', 'profile:write'] - }) - .then(function () { - return getUniqueUserAndToken(client1Id.toString('hex'), { - uid: user1.uid, - email: user1.email, - scopes: ['clients:write'] - }); - }) - .then(function () { - return getUniqueUserAndToken(client1Id.toString('hex'), { - uid: user1.uid, - email: user1.email, - scopes: ['basket', 'profile:email'] - }); - }) - .then(function () { - return getUniqueUserAndToken(client1Id.toString('hex'), { - uid: user1.uid, - email: user1.email, - scopes: ['profile:uid', 'profile', 'profile:write'] - }); - }) - .then(function () { - return Server.api.get({ - url: '/client-tokens', - headers: { - authorization: 'Bearer ' + tokenWithClientWrite - } - }); - }) - .then(function (res) { - var result = res.result; - assert.deepEqual(result[0].scope, ['basket', 'clients:write', 'profile:write']); - }); - }); - - it('errors for invalid tokens', function() { - return Server.api.get({ - url: '/client-tokens', - headers: { - authorization: 'Bearer ' + BAD_TOKEN - } - }).then(function (res) { - var result = res.result; - assert.equal(result.code, 401); - assert.equal(result.detail, 'Bearer token invalid'); - assertSecurityHeaders(res); - }); - }); - - it('errors for bad scopes', function() { - function reqWithScopes(scopes) { - return getUniqueUserAndToken(client1Id.toString('hex'), { - uid: user1.uid, - email: user1.email, - scopes: scopes - }).then(function (result) { - return Server.api.get({ - url: '/client-tokens', - headers: { - authorization: 'Bearer ' + result.token - } - }); - }); - } - - return P.all([ - reqWithScopes(['clients']), - reqWithScopes(['bar:foo:clients:write']), - reqWithScopes(['clients:write:foo']), - reqWithScopes(['clients:writ']) - ]).then(function (result) { - assert.equal(result[0].statusCode, 403); - assert.equal(result[1].statusCode, 403); - assert.equal(result[2].statusCode, 403); - assert.equal(result[3].statusCode, 403); - result.forEach(assertSecurityHeaders); - }); - }); - - it('requires auth', function() { - return Server.api.get({ - url: '/client-tokens', - headers: { - } - }).then(function (res) { - var result = res.result; - assert.equal(result.code, 401); - assert.equal(result.detail, 'Bearer token not provided'); - assertSecurityHeaders(res); - }); - }); - }); - - describe('DELETE /client-tokens/{client_id}', function() { - - it('deletes all tokens and refreshTokens for some client id', function() { - const scopes = ['profile']; - let user2ClientWriteToken; - let refreshTokenIdHash; - return db.registerClient(client2) - .then(function () { - return getUniqueUserAndToken(client2Id.toString('hex'), { - uid: user1.uid, - email: user1.email, - scopes: scopes - }); - }) - .then(function () { - return getUniqueUserAndToken(client2Id.toString('hex'), { - uid: user2.uid, - email: user2.email, - scopes: ['profile', 'clients:write'] - }); - }) - .then(function (res) { - user2ClientWriteToken = res.token; - - // also create a refreshToken for user1 - return db.generateRefreshToken({ - clientId: client2Id, - userId: buf(user1.uid), - email: user1.email, - scope: scopes - }); - }) - .then(function (t) { - refreshTokenIdHash = encrypt.hash(t.token.toString('hex')); - - return Server.api.get({ - url: '/client-tokens', - headers: { - authorization: 'Bearer ' + tokenWithClientWrite - } - }); - }) - .then(function (res) { - assert.equal(res.result.length, 2); - assertSecurityHeaders(res); - - return db.getRefreshToken(refreshTokenIdHash); - }) - .then(function (tok) { - assert.equal(tok.token.toString('hex'), refreshTokenIdHash.toString('hex'), 'refresh token was not deleted'); - - return Server.api.delete({ - url: '/client-tokens/' + client2Id.toString('hex'), - headers: { - authorization: 'Bearer ' + tokenWithClientWrite - } - }); - }) - .then(function (res) { - assert.equal(res.statusCode, 200); - assert.equal(res.payload, '{}'); - - return Server.api.get({ - url: '/client-tokens', - headers: { - authorization: 'Bearer ' + tokenWithClientWrite - } - }); - }) - .then(function (res) { - assert.equal(res.result.length, 1); - assertSecurityHeaders(res); - - return Server.api.delete({ - url: '/client-tokens/' + client1Id.toString('hex'), - headers: { - authorization: 'Bearer ' + tokenWithClientWrite - } - }); - }) - .then(function () { - return Server.api.get({ - url: '/client-tokens', - headers: { - authorization: 'Bearer ' + tokenWithClientWrite - } - }); - }) - .then(function (res) { - assert.equal(res.result.code, 401, 'client:write token was deleted'); - assert.equal(res.result.detail, 'Bearer token invalid'); - assertSecurityHeaders(res); - - return db.getRefreshToken(refreshTokenIdHash); - }) - .then(function (tok) { - assert.equal(tok, undefined, 'refresh token was deleted'); - - return Server.api.get({ - url: '/client-tokens', - headers: { - authorization: 'Bearer ' + user2ClientWriteToken - } - }); - }) - .then(function (res) { - assert.equal(res.statusCode, 200, 'user2 tokens not deleted'); - assertSecurityHeaders(res); - assert.equal(res.result.length, 1); - }); - }); - - it('deletes outstanding authorization codes for the client', () => { - let code; - mockAssertion().reply(200, mockVerifierResult({ uid: user1.uid })); - return Server.api.post({ - url: '/authorization', - payload: authParams({ - scope: 'profile', - }) - }).then(res => { - code = res.result.code; - assert.ok(code, 'an authorization code was generated'); - return Server.api.delete({ - url: '/client-tokens/' + clientId.toString('hex'), - headers: { - authorization: 'Bearer ' + tokenWithClientWrite - } - }); - }).then(res => { - return Server.api.post({ - url: '/token', - payload: { - client_id: clientId, - client_secret: secret, - code, - } - }); - }).then(res => { - assert.equal(res.statusCode, 400); - assert.equal(res.result.code, 400); - assert.equal(res.result.errno, 105); - assert.equal(res.result.message, 'Unknown code'); - assertSecurityHeaders(res); - }); - }); - - it('errors for invalid tokens', function() { - return Server.api.delete({ - url: '/client-tokens/' + clientId, - headers: { - authorization: 'Bearer ' + BAD_TOKEN - } - }).then(function (res) { - var result = res.result; - assert.equal(result.code, 401); - assert.equal(result.detail, 'Bearer token invalid'); - assertSecurityHeaders(res); - }); - }); - - it('requires auth', function() { - return Server.api.delete({ - url: '/client-tokens/' + clientId, - headers: { - } - }).then(function (res) { - var result = res.result; - assert.equal(result.code, 401); - assert.equal(result.detail, 'Bearer token not provided'); - assertSecurityHeaders(res); - }); - }); - - it('errors for bad scopes', function() { - function reqWithScopes(scopes) { - return getUniqueUserAndToken(clientId, { - scopes: scopes - }).then(function (result) { - return Server.api.delete({ - url: '/client-tokens/' + clientId, - headers: { - authorization: 'Bearer ' + result.token - } - }); - }); - } - - return P.all([ - reqWithScopes(['clients']), - reqWithScopes(['bar:foo:clients:write']), - reqWithScopes(['clients:write:foo']), - reqWithScopes(['clients:writ']) - ]).then(function (result) { - assert.equal(result[0].statusCode, 403); - assert.equal(result[1].statusCode, 403); - assert.equal(result[2].statusCode, 403); - assert.equal(result[3].statusCode, 403); - result.forEach(assertSecurityHeaders); - }); - }); - - }); - - }); -}); diff --git a/fxa-oauth-server/test/auth_bearer.js b/fxa-oauth-server/test/auth_bearer.js deleted file mode 100644 index f8d082b..0000000 --- a/fxa-oauth-server/test/auth_bearer.js +++ /dev/null @@ -1,102 +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/. */ - -/*global describe,it*/ - -const assert = require('insist'); -const mocks = require('./lib/mocks'); -const proxyquire = require('proxyquire'); -const AppError = require('../lib/error'); -const P = require('../lib/promise'); -const sinon = require('sinon'); -const ScopeSet = require('fxa-shared').oauth.scopes; - -const modulePath = '../lib/auth_bearer'; -const mockRequest = { - headers: { - authorization: 'Bearer 0000000000000000000000000000000000000000000000000000000000000000' - } -}; - -var dependencies = mocks.require([ - { path: '../../../lib/token' } -], modulePath, __dirname); - -describe('authBearer', function() { - var sandbox, authBearer; - - beforeEach(function() { - sandbox = sinon.sandbox.create(); - - sandbox.stub(dependencies['../../../lib/token'], 'verify').callsFake(function() { - return P.resolve({ - scope: ScopeSet.fromArray(['bar:foo', 'clients:write']), - user: 'bar' - }); - }); - - authBearer = proxyquire(modulePath, dependencies); - }); - - afterEach(function() { - sandbox.restore(); - }); - - it('exports auth configuration', function() { - assert.equal(authBearer.AUTH_SCHEME, 'authBearer'); - assert.equal(authBearer.AUTH_STRATEGY, 'authBearer'); - assert.equal(authBearer.SCOPE_CLIENT_WRITE, 'clients:write'); - assert.ok(authBearer.strategy); - }); - - describe('authenticate', function() { - it('provides credentials if token is valid', function(done) { - // mock hapi/lib/reply.js - var mockReply = function (err) { - throw err; - }; - - mockReply.authenticated = function (result) { - assert.equal(result.credentials.user, 'bar'); - done(); - }; - - authBearer.strategy().authenticate(mockRequest, mockReply); - }); - - it('errors if no Bearer in request', function() { - return authBearer.strategy().authenticate({ - headers: {} - }).then(assert.fail, (err) => { - assert.equal(err.output.payload.detail, 'Bearer token not provided'); - }); - }); - - it('errors if invalid token', function() { - sandbox.restore(); - sandbox = sinon.sandbox.create(); - - sandbox.stub(dependencies['../../../lib/token'], 'verify').callsFake(function() { - return P.reject(AppError.invalidToken()); - }); - - authBearer = proxyquire(modulePath, dependencies); - return authBearer.strategy().authenticate(mockRequest).then(assert.fail, (err) => { - assert.equal(err.output.payload.detail, 'Bearer token invalid'); - }); - }); - - it('errors if illegal token', function() { - authBearer = proxyquire(modulePath, dependencies); - authBearer.strategy().authenticate({ - headers: { - authorization: 'Bearer foo' - } - }).then(assert.fail, (err) => { - assert.equal(err.output.payload.detail, 'Illegal Bearer token'); - }); - }); - - }); -}); diff --git a/fxa-oauth-server/test/browserid.js b/fxa-oauth-server/test/browserid.js deleted file mode 100644 index 16208f4..0000000 --- a/fxa-oauth-server/test/browserid.js +++ /dev/null @@ -1,219 +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/. */ - -const url = require('url'); -const assert = require('insist'); -const nock = require('nock'); - -const config = require('../lib/config'); -const unique = require('../lib/unique'); - -const verifyAssertion = require('../lib/browserid'); - -const ISSUER = config.get('browserid.issuer'); -const AUDIENCE = config.get('publicUrl'); -const USERID = unique(16).toString('hex'); -const EMAIL = USERID + '@' + ISSUER; -const GENERATION = 12345; -const VERIFIED_EMAIL = 'test@exmample.com'; -const LAST_AUTH_AT = Date.now(); -const AMR = ['pwd', 'otp']; -const AAL = 2; -const PROFILE_CHANGED_AT = Date.now(); - -const ERRNO_INVALID_ASSERTION = 104; - -const GOOD_RESPONSE = { - status: 'okay', - email: EMAIL, - issuer: ISSUER, - idpClaims: { - 'fxa-generation': GENERATION, - 'fxa-verifiedEmail': VERIFIED_EMAIL, - 'fxa-lastAuthAt': LAST_AUTH_AT, - 'fxa-tokenVerified': true, - 'fxa-amr': AMR, - 'fxa-aal': AAL, - 'fxa-profileChangedAt': PROFILE_CHANGED_AT - } -}; - -const MOCK_ASSERTION = 'mock-assertion'; - -const VERIFIER_URL = url.parse(config.get('browserid.verificationUrl')); - -function mockVerifierResponse(status, body) { - return nock(VERIFIER_URL.protocol + '//' + VERIFIER_URL.host) - .post(VERIFIER_URL.path, body => { - assert.deepEqual(body, { - assertion: MOCK_ASSERTION, - audience: AUDIENCE - }); - return true; - }) - .reply(status, Object.assign({}, body)); -} - -/*global describe,it*/ - -describe('browserid verifyAssertion', function() { - - it('should accept well-formed signed assertions', () => { - mockVerifierResponse(200, GOOD_RESPONSE); - return verifyAssertion(MOCK_ASSERTION).then(claims => { - assert.deepEqual(claims, { - uid: USERID, - 'fxa-generation': GENERATION, - 'fxa-verifiedEmail': VERIFIED_EMAIL, - 'fxa-lastAuthAt': LAST_AUTH_AT, - 'fxa-tokenVerified': true, - 'fxa-amr': AMR, - 'fxa-aal': AAL, - 'fxa-profileChangedAt': PROFILE_CHANGED_AT - }); - }); - }); - - it('should reject assertions that do not validate okay', () => { - mockVerifierResponse(200, { status: 'error' }); - return verifyAssertion(MOCK_ASSERTION).then( - () => { - assert.fail('verification should have failed'); - }, - (err) => { - assert.equal(err.errno, ERRNO_INVALID_ASSERTION); - } - ); - }); - - it('should reject assertions from non-allowed issuers', () => { - mockVerifierResponse(200, Object.assign({}, GOOD_RESPONSE, { - issuer: 'evil.com' - })); - return verifyAssertion(MOCK_ASSERTION).then( - () => { - assert.fail('verification should have failed'); - }, - (err) => { - assert.equal(err.errno, ERRNO_INVALID_ASSERTION); - } - ); - }); - - it('should reject assertions where email does not match issuer', () => { - mockVerifierResponse(200, Object.assign({}, GOOD_RESPONSE, { - email: USERID + '@evil.com' - })); - return verifyAssertion(MOCK_ASSERTION).then( - () => { - assert.fail('verification should have failed'); - }, - (err) => { - assert.equal(err.errno, ERRNO_INVALID_ASSERTION); - } - ); - }); - - it('should reject assertions with email does not match issuer', () => { - mockVerifierResponse(200, Object.assign({}, GOOD_RESPONSE, { - email: USERID + '@evil.com' - })); - return verifyAssertion(MOCK_ASSERTION).then( - () => { - assert.fail('verification should have failed'); - }, - (err) => { - assert.equal(err.errno, ERRNO_INVALID_ASSERTION); - } - ); - }); - - it('should reject assertions with a malformed user id', () => { - mockVerifierResponse(200, Object.assign({}, GOOD_RESPONSE, { - email: 'non-hex-string@' + ISSUER - })); - return verifyAssertion(MOCK_ASSERTION).then( - () => { - assert.fail('verification should have failed'); - }, - (err) => { - assert.equal(err.errno, ERRNO_INVALID_ASSERTION); - } - ); - }); - - it('should reject assertions with missing `lastAuthAt` claim', () => { - const response = Object.assign({}, GOOD_RESPONSE, { - idpClaims: Object.assign({}, GOOD_RESPONSE.idpClaims) - }); - delete response['idpClaims']['fxa-lastAuthAt']; - mockVerifierResponse(200, response); - return verifyAssertion(MOCK_ASSERTION).then( - () => { - assert.fail('verification should have failed'); - }, - (err) => { - assert.equal(err.errno, ERRNO_INVALID_ASSERTION); - } - ); - }); - - it('should accept assertions with missing `amr` claim', () => { - const response = Object.assign({}, GOOD_RESPONSE, { - idpClaims: Object.assign({}, GOOD_RESPONSE.idpClaims) - }); - delete response['idpClaims']['fxa-amr']; - mockVerifierResponse(200, response); - return verifyAssertion(MOCK_ASSERTION).then(claims => { - assert.deepEqual(Object.keys(claims).sort(), [ - 'fxa-aal', - 'fxa-generation', - 'fxa-lastAuthAt', - 'fxa-profileChangedAt', - 'fxa-tokenVerified', - 'fxa-verifiedEmail', - 'uid', - ]); - }); - }); - - it('should accept assertions with missing `aal` claim', () => { - const response = Object.assign({}, GOOD_RESPONSE, { - idpClaims: Object.assign({}, GOOD_RESPONSE.idpClaims) - }); - delete response['idpClaims']['fxa-aal']; - mockVerifierResponse(200, response); - return verifyAssertion(MOCK_ASSERTION).then(claims => { - assert.deepEqual(Object.keys(claims).sort(), [ - 'fxa-amr', - 'fxa-generation', - 'fxa-lastAuthAt', - 'fxa-profileChangedAt', - 'fxa-tokenVerified', - 'fxa-verifiedEmail', - 'uid' - ]); - }); - }); - - it('should accept assertions with missing `profileChangedAt` claim', () => { - const response = Object.assign({}, GOOD_RESPONSE, { - idpClaims: Object.assign({}, GOOD_RESPONSE.idpClaims) - }); - delete response['idpClaims']['fxa-profileChangedAt']; - mockVerifierResponse(200, response); - return verifyAssertion(MOCK_ASSERTION).then(claims => { - assert.deepEqual(Object.keys(claims).sort(), [ - 'fxa-aal', - 'fxa-amr', - 'fxa-generation', - 'fxa-lastAuthAt', - 'fxa-tokenVerified', - 'fxa-verifiedEmail', - 'uid' - ]); - }); - }); - -}); diff --git a/fxa-oauth-server/test/db/helpers.js b/fxa-oauth-server/test/db/helpers.js deleted file mode 100644 index ce001db..0000000 --- a/fxa-oauth-server/test/db/helpers.js +++ /dev/null @@ -1,45 +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/. */ - -const assert = require('insist'); -const ScopeSet = require('fxa-shared').oauth.scopes; -const helpers = require('../../lib/db/helpers'); -const unique = require('../../lib/unique'); - -describe('aggregateActiveClients', function() { - var uid; - var activeClientTokens; - beforeEach(function() { - uid = unique(16).toString('hex'); - - activeClientTokens = [ - { - id: uid, - createdAt: '2017-01-26T14:28:16.219Z', - name: '123Done', - scope: ScopeSet.fromArray(['basket', 'profile:write']) - }, - { - id: uid, - createdAt: '2017-01-27T14:28:16.219Z', - name: '123Done', - scope: ScopeSet.fromArray(['clients:write']) - }, - { - id: uid, - createdAt: '2017-01-28T14:28:16.219Z', - name: '123Done', - scope: ScopeSet.fromArray(['profile']) - } - ]; - }); - - it('returns union of sorted scopes, and latest createdAt as last access time', function() { - var res = helpers.aggregateActiveClients(activeClientTokens); - assert.equal(res[0].id, uid); - assert.equal(res[0].name, '123Done'); - assert.deepEqual(res[0].scope, ['basket', 'clients:write', 'profile:write']); - assert.equal(res[0].lastAccessTime, '2017-01-28T14:28:16.219Z'); - }); -}); diff --git a/fxa-oauth-server/test/db/index.js b/fxa-oauth-server/test/db/index.js deleted file mode 100644 index 6cb989d..0000000 --- a/fxa-oauth-server/test/db/index.js +++ /dev/null @@ -1,597 +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/. */ - -const crypto = require('crypto'); - -const assert = require('insist'); -const buf = require('buf').hex; -const hex = require('buf').to.hex; -const ScopeSet = require('fxa-shared').oauth.scopes; - -const encrypt = require('../../lib/encrypt'); -const db = require('../../lib/db'); -const config = require('../../lib/config'); -const Promise = require('bluebird'); -const mock = require('../lib/mocks'); - -/*global describe,it,before*/ - -function randomString(len) { - return crypto.randomBytes(Math.ceil(len)).toString('hex'); -} - -describe('db', function() { - - describe('#_initialClients', function() { - it('should not insert already existing clients', function() { - return db.ping().then(function() { - return db._initialClients(); - }); - }); - - it('should update existing clients', function() { - var clients = config.get('clients'); - return db.ping().then(function() { - clients[0].imageUri = 'http://other.domain/foo/bar.png'; - config.set('clients', clients); - return db._initialClients(); - }).then(function() { - return db.getClient(clients[0].id); - }).then(function(c) { - assert.equal(c.imageUri, clients[0].imageUri); - }); - }); - }); - - describe('utf-8', function() { - - function makeTest(clientId, clientName) { - return function() { - var data = { - id: clientId, - name: clientName, - hashedSecret: randomString(32), - imageUri: 'https://example.domain/logo', - redirectUri: 'https://example.domain/return?foo=bar', - trusted: true - }; - - return db.registerClient(data) - .then(function(c) { - assert.equal(c.id.toString('hex'), clientId); - assert.equal(c.name, clientName); - return db.getClient(c.id); - }) - .then(function(cli) { - assert.equal(cli.id.toString('hex'), clientId); - assert.equal(cli.name, clientName); - return db.removeClient(clientId); - }) - .then(function() { - return db.getClient(clientId) - .then(function(cli) { - assert.equal(void 0, cli); - }); - }); - }; - } - - it('2-byte encoding preserved', makeTest(randomString(8), 'Düsseldorf')); - it('3-byte encoding preserved', makeTest(randomString(8), '北京')); // Beijing - it('4-byte encoding throws with mysql; ok with memdb', function() { - var data = { - id: randomString(8), - // 'MUSICAL SYMBOL F CLEF' (U+1D122) (JS: '\uD834\uDD22', UTF8: '0xF0 0x9D 0x84 0xA2') - // http://www.fileformat.info/info/unicode/char/1d122/index.htm - name: '𝄢', - hashedSecret: randomString(32), - imageUri: 'https://example.domain/logo', - redirectUri: 'https://example.domain/return?foo=bar', - trusted: true - }; - - return db.registerClient(data) - .then(function(c) { - if (config.get('db.driver') === 'memory') { - assert.ok(c.name === data.name, '4-byte UTF8 works with memory db'); - } else { - assert.fail('This should not have succeeded.'); - } - }) - .catch(function(err) { - if (config.get('db.driver') === 'memory') { - assert.fail('This should not have failed.'); - } else { - assert.ok(err); - assert.equal(err.code, 'ER_TRUNCATED_WRONG_VALUE_FOR_FIELD'); - assert.equal(err.errno, 1366); - } - }); - }); - }); - - describe('getEncodingInfo', function() { - it('should use utf8', function() { - if (config.get('db.driver') === 'memory') { - return assert.ok('getEncodingInfo has no meaning with memory impl'); - } - - return db.getEncodingInfo() - .then(function(info) { - assert.equal(info['character_set_connection'], 'utf8mb4'); - assert.equal(info['character_set_database'], 'utf8'); - assert.equal(info['collation_connection'], 'utf8mb4_unicode_ci'); - assert.equal(info['collation_database'], 'utf8_unicode_ci'); - }); - }); - }); - - describe('removeUser', function () { - var clientId = buf(randomString(8)); - var userId = buf(randomString(16)); - var email = 'a@b.c'; - var scope = ScopeSet.fromArray(['no_scope']); - var code = null; - var token = null; - var refreshToken = null; - - before(function() { - return db.registerClient({ - id: clientId, - name: 'removeUserTest', - hashedSecret: randomString(32), - imageUri: 'https://example.domain/logo', - redirectUri: 'https://example.domain/return?foo=bar', - trusted: true - }).then(function () { - return db.generateCode({ - clientId: clientId, - userId: userId, - email: email, - scope: scope, - authAt: 0 - }); - }).then(function (c) { - code = c; - return db.getCode(code); - }).then(function(code) { - assert.equal(hex(code.userId), hex(userId)); - return db.generateAccessToken({ - clientId: clientId, - userId: userId, - email: email, - scope: scope - }); - }).then(function (t) { - token = t.token; - assert.equal(hex(t.userId), hex(userId), 'token userId'); - return db.generateRefreshToken({ - clientId: clientId, - userId: userId, - email: email, - scope: scope - }); - }).then(function (t) { - refreshToken = t.token; - assert.equal(hex(t.userId), hex(userId), 'token userId'); - }); - }); - - it('should get the right refreshToken', function (){ - var hash = encrypt.hash(refreshToken); - return db.getRefreshToken(hash).then(function(t) { - assert.equal(hex(t.token), hex(hash), 'got the right refresh_token'); - }); - }); - - it('should delete tokens and codes for the given userId', function () { - return db.removeUser(userId).then(function () { - return db.getCode(code); - }).then(function (c) { - assert.equal(c, undefined, 'code deleted'); - return db.getAccessToken(token); - }).then(function (t) { - assert.equal(t, undefined, 'token deleted'); - return db.getRefreshToken(encrypt.hash(refreshToken)); - }).then(function (t) { - assert.equal(t, undefined, 'refresh_token deleted'); - }); - }); - }); - - - describe('removePublicAndCanGrantTokens', function () { - function testRemovalWithClient(clientOptions = {}) { - const clientId = buf(randomString(8)); - const userId = buf(randomString(16)); - const email = 'a@b' + randomString(16) + ' + .c'; - const scope = ['no_scope']; - let tokenIdHash; - let refreshTokenIdHash; - - return db.registerClient({ - id: clientId, - name: 'removePublicAndCanGrantTokensTest', - hashedSecret: randomString(32), - imageUri: 'https://example.domain/logo', - redirectUri: 'https://example.domain/return?foo=bar', - trusted: true, - canGrant: clientOptions.canGrant || false, - publicClient: clientOptions.publicClient || false - }).then(function () { - return db.generateAccessToken({ - clientId: clientId, - userId: userId, - email: email, - scope: scope - }); - }).then(function (t) { - tokenIdHash = encrypt.hash(t.token.toString('hex')); - return db.generateRefreshToken({ - clientId: clientId, - userId: userId, - email: email, - scope: scope - }); - }).then(function (t) { - refreshTokenIdHash = encrypt.hash(t.token.toString('hex')); - - return Promise.all([ - db.getRefreshToken(refreshTokenIdHash), - db.getAccessToken(tokenIdHash), - ]); - }).then((tokens) => { - assert.ok(tokens[0].token); - assert.ok(tokens[1].token); - return db.removePublicAndCanGrantTokens(hex(userId)); - }).then((t) => { - return Promise.all([ - db.getRefreshToken(refreshTokenIdHash), - db.getAccessToken(tokenIdHash), - ]); - }).catch((err) => { - throw err; - }); - } - - it('revokes tokens for canGrant', () => { - return testRemovalWithClient({ - canGrant: true - }).then((tokens) => { - assert.equal(tokens[0], undefined); - assert.equal(tokens[1], undefined); - }); - }); - - it('revokes tokens for publicClient', () => { - return testRemovalWithClient({ - publicClient: true - }).then((tokens) => { - assert.equal(tokens[0], undefined); - assert.equal(tokens[1], undefined); - }); - }); - - it('does not revoke tokens for not canGrant or not publicClient', () => { - return testRemovalWithClient({ - canGrant: false, - publicClient: false - }).then((tokens) => { - assert.ok(tokens[0].token); - assert.ok(tokens[1].token); - }); - }); - - it('revokes tokens for both publicClient and canGrant', () => { - return testRemovalWithClient({ - canGrant: true, - publicClient: true - }).then((tokens) => { - assert.equal(tokens[0], undefined); - assert.equal(tokens[1], undefined); - }); - }); - }); - - describe('refresh token lastUsedAt', function () { - var clientId = buf(randomString(8)); - var userId = buf(randomString(16)); - var email = 'a@b.c'; - var scope = ['no_scope']; - var code = null; - var refreshToken = null; - - beforeEach(function() { - return db.registerClient({ - id: clientId, - name: 'lastUsedAtTest', - hashedSecret: randomString(32), - imageUri: 'https://example.domain/logo', - redirectUri: 'https://example.domain/return?foo=bar', - trusted: true - }).then(function () { - return db.generateCode({ - clientId: clientId, - userId: userId, - email: email, - scope: scope, - authAt: 0 - }); - }).then(function (c) { - code = c; - return db.getCode(code); - }).then(function(code) { - assert.equal(hex(code.userId), hex(userId)); - return db.generateAccessToken({ - clientId: clientId, - userId: userId, - email: email, - scope: scope - }); - }).then(function (t) { - assert.equal(hex(t.userId), hex(userId), 'token userId'); - return db.generateRefreshToken({ - clientId: clientId, - userId: userId, - email: email, - scope: scope - }); - }).then(function (t) { - refreshToken = t; - }); - }); - - it('should refresh token lastUsedAt', function () { - var tokenFirstUsage = {}; - var hash = encrypt.hash(refreshToken.token); - - return db.getRefreshToken(hash).then(function (t) { - assert.equal(hex(t.token), hex(hash), 'same token'); - - tokenFirstUsage.createdAt = new Date(t.createdAt); - tokenFirstUsage.lastUsedAt = t.lastUsedAt; - - return Promise.delay(1000); //ensures that creation and subsequent usage are at least 1s apart - }).then(function() { - return db.usedRefreshToken(encrypt.hash(refreshToken.token)); - }).then(function() { - return db.getRefreshToken(hash); - }) - .then(function(t) { - assert.equal(hex(t.token), hex(hash), 'same token'); - var updatedLastUsedAt = new Date(t.lastUsedAt); - - assert.equal(updatedLastUsedAt > tokenFirstUsage.lastUsedAt, true, 'createdAt was updated'); - assert.equal(t.createdAt.toString(), tokenFirstUsage.createdAt.toString(), 'creation date not changed'); - }); - }); - }); - - describe('scopes', function () { - it('can register and fetch scopes', () => { - const scopeName = 'https://some-scope.mozilla.org/apps/' + Math.random(); - const notFoundScope = 'https://some-scope-404.mozilla.org'; - const newScope = { - scope: scopeName, - hasScopedKeys: true - }; - return db.registerScope(newScope) - .then(() => { - return db.getScope(notFoundScope); - }) - .then((notFoundScope) => { - assert.equal(notFoundScope, undefined); - return db.getScope(scopeName); - }).then((result) => { - assert.deepEqual(newScope, result); - }); - }); - }); - - describe('client-tokens', function () { - - describe('getActiveClientsByUid', function() { - var userId = buf(randomString(16)); - - it('should return the active clients', function() { - return db.getActiveClientsByUid(userId) - .then( - function(result) { - assert.equal(result.length, 0); - }, - function(err) { - assert.fail(err); - } - ); - }); - }); - - describe('deleteClientAuthorization', function() { - var clientId = buf(randomString(8)); - var userId = buf(randomString(16)); - - it('should delete client tokens', function() { - return db.deleteClientAuthorization(clientId, userId) - .then( - function(result) { - assert.ok(result); - }, - function(err) { - assert.fail(err); - } - ); - }); - }); - }); - - describe('developers', function () { - - describe('removeDeveloper', function() { - it('should not fail on non-existent developers', function() { - return db.removeDeveloper('unknown@developer.com'); - }); - - it('should delete developers', function() { - var email = 'email' + randomString(10) + '@mozilla.com'; - - return db.activateDeveloper(email) - .then(function(developer) { - assert.equal(developer.email, email); - - return db.removeDeveloper(email); - }) - .then(function() { - return db.getDeveloper(email); - }) - .done(function(developer) { - assert.equal(developer, null); - }); - }); - }); - - describe('getDeveloper', function() { - it('should return null if developer does not exit', function() { - return db.getDeveloper('unknown@developer.com') - .then(function(developer) { - assert.equal(developer, null); - }); - }); - - it('should throw on empty email', function() { - mock.log('db', rec => { - return rec.levelname === 'ERROR' - && rec.args[0] === 'getDeveloper'; - }); - return db.getDeveloper() - .done( - assert.fail, - function(err) { - assert.equal(err.message, 'Email is required'); - } - ); - }); - - }); - - describe('activateDeveloper and getDeveloper', function() { - it('should create developers', function() { - var email = 'email' + randomString(10) + '@mozilla.com'; - - return db.activateDeveloper(email) - .done(function(developer) { - assert.equal(developer.email, email); - }); - }); - - it('should not allow duplicates', function() { - mock.log('db', rec => { - return rec.levelname === 'ERROR' - && rec.args[0] === 'activateDeveloper'; - }); - var email = 'email' + randomString(10) + '@mozilla.com'; - - return db.activateDeveloper(email) - .then(function() { - return db.activateDeveloper(email); - }) - .done( - function() { - assert.fail(); - }, - function(err) { - assert.equal(err.message.indexOf('ER_DUP_ENTRY') >= 0, true); - } - ); - }); - - it('should throw on empty email', function() { - mock.log('db', rec => { - return rec.levelname === 'ERROR' - && rec.args[0] === 'activateDeveloper'; - }); - return db.activateDeveloper() - .done( - assert.fail, - function(err) { - assert.equal(err.message, 'Email is required'); - } - ); - }); - - }); - - describe('registerClientDeveloper and developerOwnsClient', function() { - var clientId = buf(randomString(8)); - var userId = buf(randomString(16)); - var email = 'a@b.c'; - var scope = ['no_scope']; - var code = null; - - before(function() { - return db.registerClient({ - id: clientId, - name: 'registerClientDeveloper', - hashedSecret: randomString(32), - imageUri: 'https://example.domain/logo', - redirectUri: 'https://example.domain/return?foo=bar', - trusted: true - }).then(function() { - return db.generateCode({ - clientId: clientId, - userId: userId, - email: email, - scope: scope, - authAt: 0 - }); - }).then(function(c) { - code = c; - return db.getCode(code); - }).then(function(code) { - assert.equal(hex(code.userId), hex(userId)); - return db.generateAccessToken({ - clientId: clientId, - userId: userId, - email: email, - scope: scope - }); - }).then(function(t) { - assert.equal(hex(t.userId), hex(userId), 'token userId'); - }); - }); - - it('should attach a developer to a client', function(done) { - var email = 'email' + randomString(10) + '@mozilla.com'; - - return db.activateDeveloper(email) - .then(function(developer) { - return db.registerClientDeveloper( - hex(developer.developerId), - hex(clientId) - ); - }) - .then(function() { - return db.getClientDevelopers(hex(clientId)); - }) - .done(function(developers) { - if (developers) { - var found = false; - - developers.forEach(function(developer) { - if (developer.email === email) { - found = true; - } - }); - - assert.equal(found, true); - return done(); - } - }, done); - - }); - - }); - - }); - -}); diff --git a/fxa-oauth-server/test/db/mysql.js b/fxa-oauth-server/test/db/mysql.js deleted file mode 100644 index a01c1e2..0000000 --- a/fxa-oauth-server/test/db/mysql.js +++ /dev/null @@ -1,259 +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/. */ - -const assert = require('insist'); -const sinon = require('sinon'); -const proxyquire = require('proxyquire'); -const mocks = require('../lib/mocks'); - -const modulePath = '../../lib/db/mysql'; - -var instances = {}; -var dependencies = mocks.require([ - { path: 'buf' }, - { path: 'mysql' }, - { path: 'mysql-patcher', ctor: function() { return instances.patcher; } }, - { path: '../../config' }, - { path: '../../encrypt' }, - { path: '../../scope', ctor: function() { return instances.scope; } }, - { path: '../../unique' }, - { path: './patch' } -], modulePath, __dirname); - -function nop() { -} - -function callback(cb) { - cb(); -} - -process.setMaxListeners(0); - -describe('db/mysql:', function() { - var sandbox, mysql; - - beforeEach(function() { - sandbox = sinon.createSandbox(); - - sandbox.stub(dependencies['../../config'], 'get').callsFake(function() { - return 'mock config.get result'; - }); - instances.logger = { - info: nop, - debug: nop, - warn: nop, - error: nop, - verbose: nop - }; - Object.keys(instances.logger).forEach(function(methodName) { - sandbox.spy(instances.logger, methodName); - }); - - mysql = proxyquire(modulePath, dependencies); - }); - - afterEach(function() { - sandbox.restore(); - }); - - it('exports a connect function', function() { - assert.equal(typeof mysql.connect, 'function'); - }); - - describe('connect:', function() { - beforeEach(function() { - instances.patcher = { - connect: nop, - patch: nop, - readDbPatchLevel: nop, - end: nop, - currentPatchLevel: dependencies['./patch'].level - }; - sandbox.stub(instances.patcher, 'connect').callsFake(callback); - sandbox.stub(instances.patcher, 'patch').callsFake(nop); - sandbox.stub(instances.patcher, 'end').callsFake(callback); - sandbox.stub(process, 'exit').callsFake(nop); - }); - - describe('readDbPatchLevel succeeds:', function() { - beforeEach(function() { - sandbox.stub(instances.patcher, 'readDbPatchLevel').callsFake(function(callback) { - callback(); - }); - }); - - describe('db patch level is okay:', function() { - var result; - - beforeEach(function() { - return mysql.connect({ logger: instances.logger }).then(function(r) { - result = r; - }); - }); - - it('called patcher.connect', function() { - assert.equal(instances.patcher.connect.callCount, 1); - var args = instances.patcher.connect.getCall(0).args; - assert.equal(args.length, 1); - assert.equal(typeof args[0], 'function'); - assert.equal(args[0].length, 2); - }); - - it('did not call patcher.patch', function() { - assert.equal(instances.patcher.patch.callCount, 0); - }); - - it('called patcher.readDbPatchLevel', function() { - assert.equal(instances.patcher.readDbPatchLevel.callCount, 1); - var args = instances.patcher.readDbPatchLevel.getCall(0).args; - assert.equal(args.length, 1); - assert.equal(typeof args[0], 'function'); - assert.equal(args[0].length, 1); - }); - - it('called patcher.end', function() { - assert.equal(instances.patcher.end.callCount, 1); - var args = instances.patcher.end.getCall(0).args; - assert.equal(args.length, 1); - assert.equal(typeof args[0], 'function'); - assert.equal(args[0].length, 2); - }); - - it('did not call logger.error', function() { - assert.equal(instances.logger.error.callCount, 0); - }); - - it('returned an object', function () { - assert.equal(typeof result, 'object'); - assert.notEqual(result, null); - }); - }); - - describe('db patch level is bad:', function() { - var result; - - beforeEach(function() { - instances.patcher.currentPatchLevel -= 2; - return mysql.connect({ logger: instances.logger }).catch(function(err) { - result = err; - }); - }); - - afterEach(function() { - instances.patcher.currentPatchLevel += 2; - }); - - it('called patcher.end', function() { - assert.equal(instances.patcher.end.callCount, 1); - }); - - it('called logger.error', function() { - assert.equal(instances.logger.error.callCount, 1); - var args = instances.logger.error.getCall(0).args; - assert.equal(args.length, 2); - assert.equal(args[0], 'checkDbPatchLevel'); - assert(args[1] instanceof Error); - assert.equal(args[1].message, 'unexpected db patch level: ' + (dependencies['./patch'].level - 2)); - }); - - it('returned an error', function() { - assert.ok(result instanceof Error); - }); - }); - }); - - describe('readDbPatchLevel fails:', function() { - var result; - - beforeEach(function() { - sandbox.stub(instances.patcher, 'readDbPatchLevel').callsFake(function(callback) { - callback(new Error('foo')); - }); - return mysql.connect({ logger: instances.logger }).catch(function(err) { - result = err; - }); - }); - - it('called patcher.end', function() { - assert.equal(instances.patcher.end.callCount, 1); - }); - - it('called logger.error', function() { - assert.equal(instances.logger.error.callCount, 1); - var args = instances.logger.error.getCall(0).args; - assert.equal(args[0], 'checkDbPatchLevel'); - assert.ok(args[1] instanceof Error); - assert.equal(args[1].message, 'foo'); - }); - - it('returned an error', function() { - assert.ok(result instanceof Error); - }); - }); - }); - - describe('mysql connection mode', function() { - var MysqlStore = proxyquire(modulePath, dependencies); - var store; - var mockConnection; - var mockResponses; - var capturedQueries; - - beforeEach(function() { - capturedQueries = []; - mockResponses = []; - mockConnection = { - release: sinon.spy(), - ping: sinon.spy(function(cb) { return cb(); }), - query: sinon.spy(function (q, cb) { - capturedQueries.push(q); - return cb.apply(undefined, mockResponses[capturedQueries.length - 1]); - }) - }; - - store = new MysqlStore({}); - sinon.stub(store._pool, 'getConnection').callsFake(function(cb) { - cb(null, mockConnection); - }); - }); - - it('should force new connections into strict mode', function() { - mockResponses.push([null, [{ mode: 'DUMMY_VALUE,NO_ENGINE_SUBSTITUTION' }]]); - mockResponses.push([null, []]); - return store.ping().then(function() { - assert.equal(capturedQueries.length, 2); - // The first query is checking the sql_mode. - assert.equal(capturedQueries[0], 'SELECT @@sql_mode AS mode'); - // The second query is to set the sql_mode. - assert.equal(capturedQueries[1], 'SET SESSION sql_mode = \'DUMMY_VALUE,NO_ENGINE_SUBSTITUTION,STRICT_ALL_TABLES\''); - }).then(function() { - // But re-using the connection a second time - return store.ping(); - }).then(function() { - // Should not re-issue the strict-mode queries. - assert.equal(capturedQueries.length, 2); - }); - }); - - it('should not mess with connections that already have strict mode', function() { - mockResponses.push([null, [{ mode: 'STRICT_ALL_TABLES,NO_ENGINE_SUBSTITUTION' }]]); - return store.ping().then(function() { - assert.equal(capturedQueries.length, 1); - // The only query is to check the sql_mode. - assert.equal(capturedQueries[0], 'SELECT @@sql_mode AS mode'); - }); - }); - - it('should propagate any errors that happen when setting the mode', function() { - mockResponses.push([null, [{ mode: 'SOME_NONSENSE_DEFAULT' }]]); - mockResponses.push([new Error('failed to set mode')]); - return store.ping().then(function() { - assert.fail('the ping attempt should have failed'); - }).catch(function(err) { - assert.equal(capturedQueries.length, 2); - assert.equal(err.message, 'failed to set mode'); - }); - }); - }); -}); diff --git a/fxa-oauth-server/test/db/purgeExpiredTokens.js b/fxa-oauth-server/test/db/purgeExpiredTokens.js deleted file mode 100644 index 366a944..0000000 --- a/fxa-oauth-server/test/db/purgeExpiredTokens.js +++ /dev/null @@ -1,238 +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/. */ - -const crypto = require('crypto'); - -const assert = require('insist'); -const buf = require('buf').hex; - -const db = require('../../lib/db'); -const config = require('../../lib/config'); -const auth = require('../../lib/auth'); -const Promise = require('bluebird'); - -/*global describe,it,before*/ - -function randomString(len) { - return crypto.randomBytes(Math.ceil(len)).toString('hex'); -} - -function makeTests(name, purgeMethod) { - return describe(name, function () { - var clientIdA; - var clientIdB; - var userId; - var email; - - - function seedTokens (client, userId, email, count, expiresAt) { - var accessTokens = []; - for (var i = 0; i < count; i++) { - accessTokens.push({ - clientId: buf(client), - userId: buf(userId), - email: email, - scope: [auth.SCOPE_CLIENT_MANAGEMENT], - expiresAt: expiresAt - }); - } - - return Promise.each(accessTokens, function (options) { - return db.generateAccessToken(options); - }); - } - - // Inserts 2000 access tokens with the following breakdown - // ClientIdA - 500 expired, 500 valid - // ClientIdB - 500 expired, 500 valid - before('setup clients', function(){ - email = 'asdf@asdf.com'; - clientIdA = randomString(8); - clientIdB = randomString(8); - userId = buf(randomString(16)); - - return db.registerClient({ - id: clientIdA, - name: 'ClientA', - hashedSecret: randomString(32), - imageUri: 'https://example.domain/logo', - redirectUri: 'https://example.domain/return?foo=bar', - trusted: true - }) - .then( function () { - return db.registerClient({ - id: clientIdB, - name: 'ClientB', - hashedSecret: randomString(32), - imageUri: 'https://example.domain/logo', - redirectUri: 'https://example.domain/return?foo=bar', - trusted: true - }); - }); - }); - - beforeEach('seed with tokens', function () { - return db._write('DELETE FROM tokens;') - .then( function () { - return seedTokens(clientIdA, userId, email, 500); - }) - .then( function () { - return seedTokens(clientIdB, userId, email, 500); - }) - .then( function () { - return seedTokens(clientIdA, userId, email, 500, new Date(Date.now() - (1000 * 600))); - }) - .then( function () { - return seedTokens(clientIdB, userId, email, 500, new Date(Date.now() - (1000 * 600))); - }); - }); - - it('should fail purgeExpiredTokens without ignoreClientId', function() { - return purgeMethod(1000, 5) - .then( function () { - assert.fail('purgeExpiredTokens() should fail with an empty ignoreClientId'); - }) - .catch( function (error) { - assert.equal(error.message, 'empty ignoreClientId'); - }); - }); - - it('should fail purgeExpiredTokens with an unknown ignoreClientId', function() { - var unknownClientId = 'deadbeefdeadbeef'; - return purgeMethod(1000, 5, unknownClientId) - .then( function () { - assert.fail('purgeExpiredTokens() should fail with an unknown ignoreClientId'); - }) - .catch( function (error) { - assert.equal(error.message, 'unknown ignoreClientId ' + unknownClientId); - }); - }); - - it('should call purgeExpiredTokens and ignore client', function() { - return purgeMethod(1000, 0, clientIdA, 1000) - .then( function () { - // Check clientA tokens not deleted - return db._read('SELECT COUNT(*) AS count FROM fxa_oauth.tokens WHERE clientId=UNHEX(?);', [ - clientIdA - ]); - }) - .then( function (result) { - assert.equal(result[0].count, 1000); - }) - .then( function () { - // Check clientB expired tokens are deleted - return db._read('SELECT COUNT(*) AS count FROM fxa_oauth.tokens WHERE clientId=UNHEX(?) AND expiresAt < NOW();', [ - clientIdB - ]); - }) - .then( function (result) { - assert.equal(result[0].count, 0); - }) - .then( function () { - // Check clientB unexpired tokens are not deleted - return db._read('SELECT COUNT(*) AS count FROM fxa_oauth.tokens WHERE clientId=UNHEX(?) AND expiresAt > NOW();', [ - clientIdB - ]); - }) - .then( function (result) { - assert.equal(result[0].count, 500); - }) - .then( function () { - // Check the total tokens - return db._read('SELECT COUNT(*) AS count FROM fxa_oauth.tokens;'); - }) - .then( function (result) { - assert.equal(result[0].count, 1500); - }); - }); - - if (purgeMethod === db.purgeExpiredTokens) { - // purgeExpiredTokensById cannot meet the expectations of this - // test. Not less correct, just different. - it('should call purgeExpiredTokens and only purge 100 items', function() { - return purgeMethod(100, 0, clientIdA, 1000) - .then( function () { - // Check clientA tokens not deleted - return db._read('SELECT COUNT(*) AS count FROM fxa_oauth.tokens WHERE clientId=UNHEX(?);', [ - clientIdA - ]); - }) - .then( function (result) { - assert.equal(result[0].count, 1000); - }) - .then( function () { - // Check clientB only 100 expired tokens are deleted - return db._read('SELECT COUNT(*) AS count FROM fxa_oauth.tokens WHERE clientId=UNHEX(?) AND expiresAt < NOW();', [ - clientIdB - ]); - }) - .then( function (result) { - assert.equal(result[0].count, 400); - }) - .then( function () { - // Check clientB unexpired tokens are not deleted - return db._read('SELECT COUNT(*) AS count FROM fxa_oauth.tokens WHERE clientId=UNHEX(?) AND expiresAt > NOW();', [ - clientIdB - ]); - }) - .then( function (result) { - assert.equal(result[0].count, 500); - }) - .then( function () { - // Check the total tokens - return db._read('SELECT COUNT(*) AS count FROM fxa_oauth.tokens;'); - }) - .then( function (result) { - assert.equal(result[0].count, 1900); - }); - }); - } - - it('should call purgeExpiredTokens and ignore both clients as requested', function() { - return purgeMethod(1000, 0, [ clientIdA, clientIdB ]) - .then( function () { - // Check clientA tokens not deleted - return db._read('SELECT COUNT(*) AS count FROM fxa_oauth.tokens WHERE clientId=UNHEX(?)', [ - clientIdA - ]); - }) - .then( function (result) { - assert.equal(result[0].count, 1000); - }) - .then( function () { - // Check clientB expired tokens are not deleted - return db._read('SELECT COUNT(*) AS count FROM fxa_oauth.tokens WHERE clientId=UNHEX(?)', [ - clientIdB - ]); - }) - .then( function (result) { - assert.equal(result[0].count, 1000); - }) - .then( function () { - // Check the total tokens - return db._read('SELECT COUNT(*) AS count FROM fxa_oauth.tokens'); - }) - .then( function (result) { - assert.equal(result[0].count, 2000); - }); - }); - }); - -} - -describe('db', function() { - if (config.get('db.driver') !== 'mysql') { - return Function.prototype; - } - - return makeTests('purgeExpiredTokens', db.purgeExpiredTokens); -}); - -describe('db', function() { - if (config.get('db.driver') !== 'mysql') { - return Function.prototype; - } - - return makeTests('purgeExpiredTokensById', db.purgeExpiredTokensById); -}); diff --git a/fxa-oauth-server/test/events.js b/fxa-oauth-server/test/events.js deleted file mode 100644 index b5ad12d..0000000 --- a/fxa-oauth-server/test/events.js +++ /dev/null @@ -1,105 +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/. */ - -const assert = require('insist'); -const EventEmitter = require('events'); -const proxyquire = require('proxyquire'); -const crypto = require('crypto'); -const sinon = require('sinon'); - -const UID = crypto.randomBytes(16).toString('hex'); - -let ev; -let mockConfig; -let mockDb; -let Msg; - -class SinkEmitter extends EventEmitter {} - -describe('events', function() { - beforeEach(() => { - mockDb = { - removePublicAndCanGrantTokens: sinon.stub(), - removeUser: sinon.stub() - }; - - mockConfig = { - getProperties: () => { - return { - events: { - region: 'foo', - queueUrl: 'bar' - } - }; - } - }; - - ev = proxyquire('../lib/events', { - './db': mockDb, - './config': mockConfig, - 'fxa-notifier-aws': { - Sink: SinkEmitter - }, - }); - - Msg = function Message(type, onDel) { - return { - event: type, - uid: UID, - del: onDel - }; - }; - }); - - it('is disabled when config is not', () => { - mockConfig.getProperties = () => { - return { - events: {} - }; - }; - - const mockLog = { - warn: sinon.stub() - }; - - ev = proxyquire('../lib/events', { - './config': mockConfig, - './logging': () => { - return mockLog; - } - }); - - sinon.spy(ev.start); - ev.start(); - assert.ok(mockLog.warn.calledOnce); - assert.ok(mockLog.warn.args[0][0], 'accountEvent.unconfigured'); - }); - - it('handles passwordChange event', (done) => { - ev.emit('data', new Msg('passwordChange', () => { - const revokeCall = mockDb.removePublicAndCanGrantTokens; - assert.ok(revokeCall.calledOnce); - assert.equal(revokeCall.args[0][0].length, 32, 'called with uid'); - done(); - })); - }); - - it('handles reset event', (done) => { - ev.emit('data', new Msg('reset', () => { - const revokeCall = mockDb.removePublicAndCanGrantTokens; - assert.ok(revokeCall.calledOnce); - assert.equal(revokeCall.args[0][0].length, 32, 'called with uid'); - done(); - })); - }); - - it('handles delete event', (done) => { - ev.emit('data', new Msg('delete', () => { - const revokeCall = mockDb.removeUser; - assert.ok(revokeCall.calledOnce); - assert.equal(revokeCall.args[0][0].length, 32, 'called with uid'); - done(); - })); - }); -}); diff --git a/fxa-oauth-server/test/hpkp.js b/fxa-oauth-server/test/hpkp.js deleted file mode 100644 index 7cbf53f..0000000 --- a/fxa-oauth-server/test/hpkp.js +++ /dev/null @@ -1,73 +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/. */ - -const assert = require('insist'); - -/*global describe,it,beforeEach*/ - -function clearRequireCache() { - // Delete require cache so that correct configuration values get injected when - // recreating server - delete require.cache[require.resolve('../lib/config')]; - delete require.cache[require.resolve('../lib/server')]; -} - -describe('HPKP', function () { - // Since this test starts/stops servers to test different configs - // the timeout needs to be upped - this.timeout(5000); - - var requestOptions = { - method: 'GET', - url: '/' - }; - - describe('enabled', function () { - beforeEach(function () { - process.env.HPKP_ENABLE = true; - process.env.HPKP_PIN_SHA256 = ['orlando=', 'magic=']; - process.env.HPKP_MAX_AGE = 1; - - clearRequireCache(); - }); - - it('should set report header', function (done) { - process.env.HPKP_REPORT_ONLY = false; - require('../lib/server').create().then((Server) => { - return Server.inject(requestOptions); - }).then(function (res) { - assert.equal(res.statusCode, 200); - assert.equal(res.headers['public-key-pins'], 'pin-sha256="orlando="; pin-sha256="magic="; max-age=1; includeSubdomains'); - done(); - }).catch(done); - }); - - it('should set report-only header', function (done) { - process.env.HPKP_REPORT_ONLY = true; - require('../lib/server').create().then((Server) => { - return Server.inject(requestOptions); - }).then(function (res) { - assert.equal(res.statusCode, 200); - assert.equal(res.headers['public-key-pins-report-only'], 'pin-sha256="orlando="; pin-sha256="magic="; max-age=1; includeSubdomains'); - done(); - }).catch(done); - }); - }); - - describe('disabled', function () { - it('should set no header', function (done) { - process.env.HPKP_ENABLE = false; - - clearRequireCache(); - require('../lib/server').create().then((Server) => { - return Server.inject(requestOptions); - }).then(function (res) { - assert.equal(res.statusCode, 200); - assert.equal(res.headers['public-key-pins'], undefined); - assert.equal(res.headers['public-key-pins-report-only'], undefined); - done(); - }).catch(done); - }); - }); -}); diff --git a/fxa-oauth-server/test/lib/mocks.js b/fxa-oauth-server/test/lib/mocks.js deleted file mode 100644 index f0157d1..0000000 --- a/fxa-oauth-server/test/lib/mocks.js +++ /dev/null @@ -1,70 +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/. */ - -const path = require('path'); - -module.exports = { - require: requireDependencies, - log: mockLog -}; - -// `mocks.require` -// -// Require dependencies using the same path that is specified in the module -// under test. -// -// Returns an object containing dependencies keyed by their path. -// -// Expects three arguments; `dependencies`, `modulePath` and `basePath`. -// -// dependencies: Array of { path, ctor } items. -// path: The dependency path, as specified in the module -// under test. -// ctor: Optional. If the dependency is a constructor for -// an instance that you wish to mock, set this to a -// function that returns your mock instance. -// modulePath: The relative path to the module under test. -// basePath: The base path, i.e. __dirname for the test itself. -function requireDependencies(dependencies, modulePath, basePath) { - var result = {}; - - dependencies.forEach(function (dependency) { - result[dependency.path] = requireDependency(dependency, modulePath, basePath); - }); - - return result; -} - -function requireDependency(dependency, modulePath, basePath) { - if (typeof dependency.ctor === 'function') { - return dependency.ctor; - } - - var localPath = dependency.path; - - if (localPath[0] === '.') { - localPath = path.relative( - basePath, - path.resolve(basePath, modulePath, localPath) - ); - } - - return require(localPath); -} - -function mockLog(logger, cb) { - var root = require('../../lib/logging')(); - var log = require('../../lib/logging')(logger); - var filter = { - filter: function(record) { - if (cb(record)) { - log.removeFilter(filter); - log.setLevel(root.getEffectiveLevel()); - return false; - } - return true; - } - }; - log.addFilter(filter); -} diff --git a/fxa-oauth-server/test/lib/privkey.json b/fxa-oauth-server/test/lib/privkey.json deleted file mode 100644 index 714596a..0000000 --- a/fxa-oauth-server/test/lib/privkey.json +++ /dev/null @@ -1 +0,0 @@ -{"kty":"RSA","n":"xaQHsKpu1KSK-YEMoLzZS7Xxciy3fsGrhrrqW_JBrq3IRmeGLaqlE80zcpIVnStyp9tbet2niYTemt8ug591YWO5Y-S0EgQyFTxnGjzNOvAL6Cd2iGie9QeSehfFLNyRPdQiadYw07fw-h5gweMpVJv8nTgS-Bcorlw9JQM6Il1cUpbP0Lt-F_5qrzlaOiTEAAb4JGOusVh0n-MZfKt7w0mikauMH5KfhflwQDn4YTzRkWJzlldXr1Cs0ZkYzOwS4Hcoku7vd6lqCUO0GgZvkuvCFqdVKzpa4CGboNdfIjcGVF4f1CTQaQ0ao51cwLzq1pgi5aWYhVH7lJcm6O_BQw","e":"AQAB","d":"EM21aavT6Hhk6Hm0XSYxQ2KguJhcsYY90yKpMlASjYtw76t1mQRdLKXRrfgFpms_QE5CJNwblnGZi4lWJxKzpCgaZwfW14FL0Mpl6bEpsc0e9goE5ewfN64BIihLN1k5cAxNMLppRFbrQhi7GUD7DpqEi8lss3Mknk5xVGhF1Q38i5wSPLaLNgdt7QUIRdCCsrVFwnj83e8Rmmchr2-LXg2P_2KbVwdKfLuDiaYgDr2OELiK3VZa3WMexLrQHXGf1bvuK9xg6DNQ5Oe3slNWe7a0cpNR5oPX8HjqREmKciCFxHSA5o0ogyu5YvVjvZuh4Fm1iAM1fJNzYpabd_D8IQ","p":"-WA4WDmaTg5gD2h9Cl7hvgF7-77CNV7nrNrfXZNVMpBKM9z2X1188BtDLI7AytnpJqEyrYqTDs6gmwQ2sLAgoUoe1GVVzeBvz__2lZ_OAaf74WJ8PSgJI36YqM9-44TMgjForqYqg9j75yTxisRIflTvsbh5HV1VvelUm0vEAT0","q":"yuQBk03zbk-zIBRtx7s_sSAs3wEprqqn7za8-ejpurMMBM6vKzZ6UykDEgUy5aHmd75ygNwc2v3RJF7cw0DrLZ6gMIkAYMh3e-MMN-2tFwkFjlA0ccttkN2LZUFGKdmO6J37_uoR3YwDZHCTnyBR5cyoWwtTMF6GEx9_pfXp9H8","dp":"pRW-lyEi9lNr4idgx5HCTV98LU6-EEjQg8ytG6xiNUPx6112t_ESuXzCvmeOV3tkbk8-VkYrTh0ZkyV58wPVxhBkUmT3JYBTZNXk7m5JGS2UgEMLTg0H57hx5SbfsEyEehetXhjggkINmJoLULrZ5s_hkbw2aWsVTNB_UwMYMV0","dq":"Lmsv-RWyhiBx6PsDitjKX6nu2i6X7MBan540ajDhmLdyHn9zED3siq4tZ6gM1wDNi8PkypqRd4DuopWZiIqHw-4w1CnkDkCPJabymrEkEssbnE25Ufeq36PwSoA-n0CJM6tBhjbjU36_H_GptJReaGcEdaAHrl9R6XohaET0-90","qi":"tFAgjYANe4t3Xj2blS5jzZohwl1ZuQNf5vf9QFVx6H3j8JMYjKvUZOrXBxOXwQmvZ3TKpYGxjij9RYfVfLnB14V1q4Hkty35ImoQVCSUriYP_pr9g2S9HH0O0hbWzOitQ13tlxeH7-bcXwUkM3SzLQDaYaWRNCyQBcSReKm0RZE"} diff --git a/fxa-oauth-server/test/lib/pubkey.json b/fxa-oauth-server/test/lib/pubkey.json deleted file mode 100644 index 9681103..0000000 --- a/fxa-oauth-server/test/lib/pubkey.json +++ /dev/null @@ -1 +0,0 @@ -{"kty": "RSA","n":"xaQHsKpu1KSK-YEMoLzZS7Xxciy3fsGrhrrqW_JBrq3IRmeGLaqlE80zcpIVnStyp9tbet2niYTemt8ug591YWO5Y-S0EgQyFTxnGjzNOvAL6Cd2iGie9QeSehfFLNyRPdQiadYw07fw-h5gweMpVJv8nTgS-Bcorlw9JQM6Il1cUpbP0Lt-F_5qrzlaOiTEAAb4JGOusVh0n-MZfKt7w0mikauMH5KfhflwQDn4YTzRkWJzlldXr1Cs0ZkYzOwS4Hcoku7vd6lqCUO0GgZvkuvCFqdVKzpa4CGboNdfIjcGVF4f1CTQaQ0ao51cwLzq1pgi5aWYhVH7lJcm6O_BQw","e":"AQAB"} diff --git a/fxa-oauth-server/test/lib/server.js b/fxa-oauth-server/test/lib/server.js deleted file mode 100644 index 88e9d1b..0000000 --- a/fxa-oauth-server/test/lib/server.js +++ /dev/null @@ -1,59 +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/. */ - -const P = require('../../lib/promise'); -const Server = require('../../lib/server'); -const Internal = require('../../lib/server/internal'); -const version = require('../../lib/config').get('api.version'); - -function wrapServer(serverPromise) { - var wrap = {}; - function request(options) { - return new P(resolve => { - return serverPromise.then((s) => { - return s.inject(options); - }).then(resolve); - }); - } - - function opts(options) { - if (typeof options === 'string') { - options = { url: options }; - } - return options; - } - - wrap.post = function post(options) { - options = opts(options); - options.method = 'POST'; - return request(options); - }; - - wrap.get = function get(options) { - options = opts(options); - options.method = 'GET'; - return request(options); - }; - - wrap.delete = function _delete(options) { - options = opts(options); - options.method = 'DELETE'; - return request(options); - }; - - var api = {}; - Object.keys(wrap).forEach(function(key) { - api[key] = function api(options) { - options = opts(options); - options.url = '/v' + version + options.url; - return wrap[key](options); - }; - }); - - wrap.api = api; - return wrap; -} - -module.exports = wrapServer(Server.create()); -module.exports.internal = wrapServer(Internal.create()); diff --git a/fxa-oauth-server/test/lib/util.js b/fxa-oauth-server/test/lib/util.js deleted file mode 100644 index 45bccf3..0000000 --- a/fxa-oauth-server/test/lib/util.js +++ /dev/null @@ -1,25 +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/. */ - -const assert = require('insist'); -const config = require('../../lib/config').getProperties(); - -function assertSecurityHeaders(res) { - const expect = { - 'strict-transport-security': 'max-age=15552000; includeSubDomains', - 'x-content-type-options': 'nosniff', - 'x-xss-protection': '1; mode=block', - 'x-frame-options': 'DENY' - }; - - Object.keys(expect).forEach(function(header) { - assert.equal(res.headers[header], expect[header]); - }); - - assert.equal(res.headers['cache-control'], config.cacheControl); -} - -module.exports = { - assertSecurityHeaders: assertSecurityHeaders -}; diff --git a/fxa-oauth-server/test/routes/authorization.js b/fxa-oauth-server/test/routes/authorization.js deleted file mode 100644 index b8eea97..0000000 --- a/fxa-oauth-server/test/routes/authorization.js +++ /dev/null @@ -1,120 +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 assert = require('insist'); -const Joi = require('joi'); -const route = require('../../lib/routes/authorization'); -const validation = route.validate.payload; - -const CLIENT_ID = '98e6508e88680e1b'; -// jscs:disable -const BASE64URL_STRING = 'TG9yZW0gSXBzdW0gaXMgc2ltcGx5IGR1bW15IHRleHQgb2YgdGhlIHByaW50aW5nIGFuZCB0eXBlc2V0dGluZyBpbmR1c3RyeS4gTG9yZW0gSXBzdW0gaGFzIGJlZW4gdGhlIGluZHVzdHJ5J3Mgc3RhbmRhcmQgZHVtbXkgdGV4dCBldmVyIHNpbmNlIHRoZSAxNTAwcywgd2hlbiBhbiB1bmtub3duIHByaW50ZXIgdG9vayBhIGdhbGxleSBvZiB0eXBlIGFuZCBzY3JhbWJsZWQgaXQgdG8gbWFrZSBhIHR5cGUgc3BlY2ltZW4gYm9v'; -// jscs:enable -const PKCE_CODE_CHALLENGE = 'iyW5ScKr22v_QL-rcW_EGlJrDSOymJvrlXlw4j7JBiQ'; -const PKCE_CODE_CHALLENGE_METHOD = 'S256'; - -function joiAssertFail(req, param, messagePostfix) { - messagePostfix = messagePostfix || 'is required'; - let fail = null; - - try { - Joi.assert(req, validation); - } catch (err) { - fail = true; - assert.ok(err.isJoi); - assert.ok(err.name, 'ValidationError'); - assert.equal(err.details[0].message, `"${param}" ${messagePostfix}`); - } - - if (! fail) { - throw new Error('Did not throw!'); - } -} - -describe('/authorization POST', function () { - it('fails with no client_id', () => { - joiAssertFail({ - foo: 1 - }, 'client_id'); - }); - - it('fails with no assertion', () => { - joiAssertFail({ - client_id: CLIENT_ID - }, 'assertion'); - }); - - it('fails with no state', () => { - joiAssertFail({ - client_id: CLIENT_ID, - assertion: BASE64URL_STRING, - }, 'state'); - }); - - it('fails with no state', () => { - Joi.assert({ - client_id: CLIENT_ID, - assertion: BASE64URL_STRING, - state: 'foo' - }, validation); - }); - - describe('PKCE params', function () { - it('accepts code_challenge and code_challenge_method', () => { - Joi.assert({ - client_id: CLIENT_ID, - assertion: BASE64URL_STRING, - state: 'foo', - code_challenge: PKCE_CODE_CHALLENGE, - code_challenge_method: PKCE_CODE_CHALLENGE_METHOD, - }, validation); - }); - - it('accepts code_challenge and code_challenge_method', () => { - joiAssertFail({ - client_id: CLIENT_ID, - assertion: BASE64URL_STRING, - state: 'foo', - code_challenge: PKCE_CODE_CHALLENGE, - code_challenge_method: 'bad_method', - }, 'code_challenge_method', 'must be one of [S256]'); - }); - - it('validates code_challenge', () => { - joiAssertFail({ - client_id: CLIENT_ID, - assertion: BASE64URL_STRING, - state: 'foo', - code_challenge: 'foo', - code_challenge_method: PKCE_CODE_CHALLENGE_METHOD, - }, 'code_challenge', 'length must be 43 characters long'); - }); - - it('works with response_type code (non-default)', () => { - Joi.assert({ - client_id: CLIENT_ID, - assertion: BASE64URL_STRING, - state: 'foo', - code_challenge: PKCE_CODE_CHALLENGE, - code_challenge_method: PKCE_CODE_CHALLENGE_METHOD, - response_type: 'code' - }, validation); - }); - - it('fails with response_type token', () => { - joiAssertFail({ - client_id: CLIENT_ID, - assertion: BASE64URL_STRING, - state: 'foo', - code_challenge: PKCE_CODE_CHALLENGE, - code_challenge_method: PKCE_CODE_CHALLENGE_METHOD, - response_type: 'token' - }, 'code_challenge_method', 'is not allowed'); - }); - - }); - -}); diff --git a/fxa-oauth-server/test/routes/token.js b/fxa-oauth-server/test/routes/token.js deleted file mode 100644 index fbfe73f..0000000 --- a/fxa-oauth-server/test/routes/token.js +++ /dev/null @@ -1,195 +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/. */ - -const assert = require('insist'); -const Joi = require('joi'); -const route = require('../../lib/routes/token'); - -const CLIENT_SECRET = 'b93ef8a8f3e553a430d7e5b904c6132b2722633af9f03128029201d24a97f2a8'; -const CLIENT_ID = '98e6508e88680e1b'; -const CODE = 'df6dcfe7bf6b54a65db5742cbcdce5c0a84a5da81a0bb6bdf5fc793eef041fc6'; -const PKCE_CODE_VERIFIER = 'au3dqDz2dOB0_vSikXCUf4S8Gc-37dL-F7sGxtxpR3R'; -const GRANT_JWT = 'urn:ietf:params:oauth:grant-type:jwt-bearer'; - -function joiRequired(err, param) { - assert.ok(err.isJoi); - assert.ok(err.name, 'ValidationError'); - assert.equal(err.details[0].message, `"${param}" is required`); -} - -function joiNotAllowed(err, param) { - assert.ok(err.isJoi); - assert.ok(err.name, 'ValidationError'); - assert.equal(err.details[0].message, `"${param}" is not allowed`); -} - -describe('/token POST', function () { - // route validation function - function v(req, ctx, cb) { - if (typeof ctx === 'function' && ! cb) { - cb = ctx; - ctx = undefined; - } - Joi.validate(req, route.validate.payload, {context: ctx}, cb); - } - - it('fails with no client_id', (done) => { - v({ - client_secret: CLIENT_SECRET, - code: CODE - }, (err) => { - joiRequired(err, 'client_id'); - done(); - }); - }); - - it('valid client_secret scheme', (done) => { - v({ - client_id: CLIENT_ID, - client_secret: CLIENT_SECRET, - code: CODE - }, (err) => { - assert.equal(err, null); - done(); - }); - }); - - it('requires client_secret', (done) => { - v({ - client_id: CLIENT_ID, - code: CODE - }, (err) => { - joiRequired(err, 'client_secret'); - done(); - }); - }); - - it('forbids client_id when authz header provided', (done) => { - v({ - client_id: CLIENT_ID - }, { - headers: { - authorization: 'Basic ABCDEF' - } - }, (err) => { - joiNotAllowed(err, 'client_id'); - done(); - }); - }); - - it('forbids client_secret when authz header provided', (done) => { - v({ - client_secret: CLIENT_SECRET - }, { - headers: { - authorization: 'Basic ABCDEF' - } - }, (err) => { - joiNotAllowed(err, 'client_secret'); - done(); - }); - }); - - describe('pkce', () => { - it('accepts pkce code_verifier instead of client_secret', (done) => { - v({ - client_id: CLIENT_ID, - code_verifier: PKCE_CODE_VERIFIER, - code: CODE - }, (err) => { - assert.equal(err, null); - done(); - }); - }); - - it('rejects pkce code_verifier that is too small', (done) => { - const bad_code_verifier = PKCE_CODE_VERIFIER.substring(0, 32); - v({ - client_id: CLIENT_ID, - code_verifier: bad_code_verifier, - code: CODE - }, (err) => { - assert.ok(err.isJoi); - assert.ok(err.name, 'ValidationError'); - assert.equal(err.details[0].message, `"code_verifier" length must be at least 43 characters long`); // eslint-disable-line quotes - done(); - }); - }); - - it('rejects pkce code_verifier that is too big', (done) => { - const bad_code_verifier = PKCE_CODE_VERIFIER + PKCE_CODE_VERIFIER + PKCE_CODE_VERIFIER + PKCE_CODE_VERIFIER; - v({ - client_id: CLIENT_ID, - code_verifier: bad_code_verifier, - code: CODE - }, (err) => { - assert.ok(err.isJoi); - assert.ok(err.name, 'ValidationError'); - assert.equal(err.details[0].message, `"code_verifier" length must be less than or equal to 128 characters long`); // eslint-disable-line quotes - done(); - }); - }); - - it('rejects pkce code_verifier that contains invalid characters', (done) => { - const bad_code_verifier = PKCE_CODE_VERIFIER + ' :.'; - v({ - client_id: CLIENT_ID, - code_verifier: bad_code_verifier, - code: CODE - }, (err) => { - assert.ok(err.isJoi); - assert.ok(err.name, 'ValidationError'); - assert.equal(err.details[0].message, `"code_verifier" with value "${bad_code_verifier}" fails to match the required pattern: /^[A-Za-z0-9-_]+$/`); - done(); - }); - }); - }); - - describe('grant_type JWT', () => { - it('forbids client_id', (done) => { - v({ - client_id: CLIENT_ID, - client_secret: CLIENT_SECRET, - code: CODE, - grant_type: GRANT_JWT, - }, (err) => { - joiNotAllowed(err, 'client_id'); - done(); - }); - }); - - it('forbids client_secret', (done) => { - v({ - client_secret: CLIENT_SECRET, - code: CODE, - grant_type: GRANT_JWT, - }, (err) => { - joiNotAllowed(err, 'client_secret'); - done(); - }); - }); - - it('forbids code_verifier', (done) => { - v({ - code_verifier: PKCE_CODE_VERIFIER, - grant_type: GRANT_JWT, - }, (err) => { - joiNotAllowed(err, 'code_verifier'); - done(); - }); - }); - - it('forbids code', (done) => { - v({ - code: CODE, - grant_type: GRANT_JWT, - }, (err) => { - joiNotAllowed(err, 'code'); - done(); - }); - }); - - }); - -}); diff --git a/fxa-oauth-server/test/server.js b/fxa-oauth-server/test/server.js deleted file mode 100644 index 43a1029..0000000 --- a/fxa-oauth-server/test/server.js +++ /dev/null @@ -1,124 +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/. */ - -const assert = require('insist'); -const util = require('util'); - -const Server = require('./lib/server'); - -const assertSecurityHeaders = require('./lib/util').assertSecurityHeaders; - -/*global describe,it*/ - -function checkVersionAndHeaders(path) { - return function(done) { - Server.get(path).then(function(res) { - assert.equal(res.statusCode, 200); - assert.equal(res.result.version, require('../package.json').version); - assert.deepEqual(Object.keys(res.result), ['version', 'commit', 'source' ]); - assert(res.result.source); - assert(res.result.commit); - assert.ok(res.result.commit.match(/^[0-9a-f]{40}$/)); - assertSecurityHeaders(res); - - // but the other security builtin headers from hapi are not set - var other = { - 'x-download-options': 1, - }; - - Object.keys(res.headers).forEach(function(header) { - assert.ok(! other[header.toLowerCase()]); - }); - }).done(done, done); - }; -} - -describe('server', function() { - describe('/', function() { - it('should return the version', checkVersionAndHeaders('/')); - }); - - describe('/__version__', function() { - it('should return the version', checkVersionAndHeaders('/__version__')); - }); - - describe('/__heartbeat__', function() { - it('should succeed', function(done) { - Server.get('/__heartbeat__').then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - }).done(done, done); - }); - }); - - describe('/__lbheartbeat__', function() { - it('should succeed', function(done) { - Server.get('/__lbheartbeat__').then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - }).done(done, done); - }); - }); - - describe('/config', function() { - it('should succeed', function(done) { - Server.get('/config').then(function(res) { - assert.equal(res.statusCode, 200); - assertSecurityHeaders(res); - assert(res.result.browserid.issuer); - assert(res.result.browserid.verificationUrl); - assert(res.result.contentUrl); - }).done(done, done); - }); - }); - - describe('a large request body', function() { - var args = { token: '' }; - var argslen = JSON.stringify(args).length; - const HAPI_PAYLOAD_MAXBYTES = 16384; // see '../lib/server/config.js' - var blob = new Array(HAPI_PAYLOAD_MAXBYTES - argslen + 1).join('a'); - - it('below the limit, returns 40? with ???', function(done) { - var content = util._extend(args); - content.token = blob; - content = JSON.stringify(content); - - Server.api.post({ - url: '/token', - payload: content, - headers: { - 'content-length': content.length - } - }).then(function(res) { - assert.equal(res.statusCode, 400); - assertSecurityHeaders(res); - assert.equal(res.result.errno, 109); - assert.equal(res.result.error, 'Bad Request'); - assert.equal(res.result.message, 'Invalid request parameter'); - }).done(done, done); - }); - - it('above the limit, returns 400 with Payload too large', function(done) { - var content = util._extend(args); - content.token = blob + 'a'; // one byte over the limit - content = JSON.stringify(content); - - Server.api.post({ - url: '/token', - payload: content, - headers: { - 'content-length': content.length - } - }).then(function(res) { - var result = res.result; - assert.equal(res.statusCode, 413); - assertSecurityHeaders(res); - assert.equal(result.errno, 999); - assert.equal(result.error, 'Request Entity Too Large'); - var message = result.message; - assert.equal(message.indexOf('Payload content length greater'), 0); - }).done(done, done); - }); - }); -}); diff --git a/fxa-oauth-server/test/util.js b/fxa-oauth-server/test/util.js deleted file mode 100644 index 4342361..0000000 --- a/fxa-oauth-server/test/util.js +++ /dev/null @@ -1,19 +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/. */ - -const assert = require('insist'); -const util = require('../lib/util'); - -describe('util', function () { - describe('base64URLEncode', function () { - it('properly encodes', function () { - var testBase64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', - testBuffer = Buffer.from(testBase64, 'base64'), - expectedBase64 = testBase64.replace('+', '-').replace('/', '_'); - - assert.equal(util.base64URLEncode(testBuffer), expectedBase64); - }); - - }); -});